Foundations of Python Network Programming - 读书笔记系列(1) - Low-Level Networking
以前,人们热衷于如何将两台机器互相连接,许多连接的方法在今天已经过时,还有很多方法沿用至今。TCP/IP就是之一,可以说,TCP/IP协议是当今使用范围最广的协议,这本书所有的内容都是基于TCP/IP的。TCP/IP的数据传输层是TCP和UDP,我们通过TCP和UDP连接远程机器时,只需要远程机器的IP和端口号,然后建立连接传输数据。其中TCP和UDP又有着许多不同之处。
何时使用TCP?
1. 你需要确保传输的数据准确的到达并且保持完整。
2. 你需要发送大量的数据,而不是简单的请求和返回。
3. 你能忍受建立连接时消耗的时间。(效率低)
何时使用UDP?
1. 你不关心你发送的包是否准确的到达,或者你能自己处理这些问题。(不稳定)
2. 你只是希望得到一个简单的请求和返回。
3. 你需要快速的建立连接。(效率高)
4. 你发送的数据量不是很大。UDP限制每个包不能超过64KB,通常人们使用UDP时只使用了低于1KB。
在Python中建立一个TCP或UDP连接是一件非常简单的事情,需要使用Socket模块,这是Python的标准模块。
我们上面连接的是一个http站点,默认端口是80,我们可以通过下面的方法获取到默认的端口号:
3. 连接后,从一个socket对象获取信息
比如,获取本机的IP地址和端口号,获取远程机器的IP地址和端口号,如:
Creating socket... done.
Looking up port number... done.
Connecting to remote host on port 80... done.
Connected from ('192.168.XX.XX', 2548)
Connected to ('64.233.189.104', 80)
可以看到,我的本机使用的是一个随机的端口号(2548),每次执行端口号都会不同。
4. File-like 对象
我们可以通过Socket对象来执行一些比如发送(send(), sendto()),接收数据的操作(recv(), recvfrom()),同时,我们还可以把Socket对象转换为一个类似文件的对象(File-like Object),然后使用其中的write()来发送数据,read(), readline()来接收数据。
File-like对象更适合TCP连接,因为TCP连接必须保证数据流能够完整正确的到达,数据流表现的更像是一个文件。而UDP却不是,它是一个基于包的连接,它只管把这些包发送出去,如果使用File-like对象来处理,将很难追踪定位出现的错误。生成一个File-like对象通过下面的语句:
注意上面在我们发送了数据之后,使用了shutdown方法,是为了保证发送的数据成功到达目标机器。因为shutdown()会等待,直到接收到一个准确的退出代码。
1. 创建一个socket对象。(create socket object)
2. 设置socket对象的属性。(set options)
3. 绑定一个端口。(bind to a port)
4. 监听来自客户端的连接。(listen for connection)
针对上面的四个步骤,下面是一个最简单的实现:
通过UDP创建一个服务端步骤也差不多,创建一个socket,设置option,bind端口,然而,UDP不需要listen()和accept(),而是使用recvfrom()就足够了。recvfrom()函数返回两个信息:接受的数据(data)和客户端的地址(address)和端口(port)。下面是UDP服务端的例子:
socket.getaddrinfo()根据主机名或域名等来获取相应的信息。
返回值是一个tuple的列表,每个tuple返回如下信息:
同时,gethostbyaddr()根据IP地址获取相应的信息,同时使用getaddrinfo()和gethostbyaddr()可以实现对域名的双重验证。如下面的例子:
OK,第一部分就到这里。
何时使用TCP?
1. 你需要确保传输的数据准确的到达并且保持完整。
2. 你需要发送大量的数据,而不是简单的请求和返回。
3. 你能忍受建立连接时消耗的时间。(效率低)
何时使用UDP?
1. 你不关心你发送的包是否准确的到达,或者你能自己处理这些问题。(不稳定)
2. 你只是希望得到一个简单的请求和返回。
3. 你需要快速的建立连接。(效率高)
4. 你发送的数据量不是很大。UDP限制每个包不能超过64KB,通常人们使用UDP时只使用了低于1KB。
在Python中建立一个TCP或UDP连接是一件非常简单的事情,需要使用Socket模块,这是Python的标准模块。
客户端(Network Clients)
1. 创建一个socket对象s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
第一个参数socket.AF_INET说明我们使用的是IPv4,第二个参数socket.SOCK_STREAM指的是我们使用TCP进行数据传输,如果要使用UDP,则使用socket.SOCK_DGRAM,如:s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
2. connect连接远程服务器s.connect(("www.example.com", 80))
连接远程服务器需要远程服务器的IP和端口,注意到上面我们使用了服务器的域名也是可以的,因为Python为我们做了DNS的解析。同时,注意到connect的参数是一个tuple。我们上面连接的是一个http站点,默认端口是80,我们可以通过下面的方法获取到默认的端口号:
port = socket.getservbyname('http', 'tcp')
相应的,你可以查询诸如:smtp,ftp等等端口号。3. 连接后,从一个socket对象获取信息
比如,获取本机的IP地址和端口号,获取远程机器的IP地址和端口号,如:
#!/usr/bin/env python
# Information Example - Chapter 2
import socket
print "Creating socket",
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print "done."
print "Looking up port number",
port = socket.getservbyname('http', 'tcp')
print "done."
print "Connecting to remote host on port %d" % port,
s.connect(("www.google.com", port))
print "done."
print "Connected from", s.getsockname()
print "Connected to", s.getpeername()
输出结果会显示:# Information Example - Chapter 2
import socket
print "Creating socket",
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print "done."
print "Looking up port number",
port = socket.getservbyname('http', 'tcp')
print "done."
print "Connecting to remote host on port %d" % port,
s.connect(("www.google.com", port))
print "done."
print "Connected from", s.getsockname()
print "Connected to", s.getpeername()
Creating socket... done.
Looking up port number... done.
Connecting to remote host on port 80... done.
Connected from ('192.168.XX.XX', 2548)
Connected to ('64.233.189.104', 80)
可以看到,我的本机使用的是一个随机的端口号(2548),每次执行端口号都会不同。
4. File-like 对象
我们可以通过Socket对象来执行一些比如发送(send(), sendto()),接收数据的操作(recv(), recvfrom()),同时,我们还可以把Socket对象转换为一个类似文件的对象(File-like Object),然后使用其中的write()来发送数据,read(), readline()来接收数据。
File-like对象更适合TCP连接,因为TCP连接必须保证数据流能够完整正确的到达,数据流表现的更像是一个文件。而UDP却不是,它是一个基于包的连接,它只管把这些包发送出去,如果使用File-like对象来处理,将很难追踪定位出现的错误。生成一个File-like对象通过下面的语句:
fd = s.makefile('rw', 0) #s 是前面的创建的socket对象,rw表示可读和可写权限
然后,就可以调用fd的write(), readines()等方法了。例子如下,同时注意细节的错误处理,这里不详细介绍:#!/usr/bin/env python
# Error Handling Example With Shutdown and File-Like Objects - Chapter 2
import socket, sys, time
host = sys.argv[1]
textport = sys.argv[2]
filename = sys.argv[3]
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, e:
print "Strange error creating socket: %s" % e
sys.exit(1)
# Try parsing it as a numeric port number.
try:
port = int(textport)
except ValueError:
# That didn't work. Look it up instread.
try:
port = socket.getservbyname(textport, 'tcp')
except socket.error, e:
print "Couldn't find your port: %s" % e
sys.exit(1)
try:
s.connect((host, port))
except socket.gaierror, e:
print "Address-related error connecting to server: %s" % e
sys.exit(1)
except socket.error, e:
print "Connection error: %s" % e
sys.exit(1)
fd = s.makefile('rw', 0)
print "sleeping"
time.sleep(10)
print "Continuing."
try:
fd.write("GET %s HTTP/1.0\r\n\r\n" % filename)
except socket.error, e:
print "Error sending data: %s" % e
sys.exit(1)
try:
fd.flush()
except socket.error, e:
print "Error sending data (detected by flush): %s" % e
sys.exit(1)
try:
s.shutdown(1)
except socket.error, e:
print "Error sending data (detected by shutdown): %s" % e
sys.exit(1)
while 1:
try:
buf = fd.read(2048)
except socket.error, e:
print "Error receiving data: %s" % e
sys.exit(1)
if not len(buf):
break
sys.stdout.write(buf)
# Error Handling Example With Shutdown and File-Like Objects - Chapter 2
import socket, sys, time
host = sys.argv[1]
textport = sys.argv[2]
filename = sys.argv[3]
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, e:
print "Strange error creating socket: %s" % e
sys.exit(1)
# Try parsing it as a numeric port number.
try:
port = int(textport)
except ValueError:
# That didn't work. Look it up instread.
try:
port = socket.getservbyname(textport, 'tcp')
except socket.error, e:
print "Couldn't find your port: %s" % e
sys.exit(1)
try:
s.connect((host, port))
except socket.gaierror, e:
print "Address-related error connecting to server: %s" % e
sys.exit(1)
except socket.error, e:
print "Connection error: %s" % e
sys.exit(1)
fd = s.makefile('rw', 0)
print "sleeping"
time.sleep(10)
print "Continuing."
try:
fd.write("GET %s HTTP/1.0\r\n\r\n" % filename)
except socket.error, e:
print "Error sending data: %s" % e
sys.exit(1)
try:
fd.flush()
except socket.error, e:
print "Error sending data (detected by flush): %s" % e
sys.exit(1)
try:
s.shutdown(1)
except socket.error, e:
print "Error sending data (detected by shutdown): %s" % e
sys.exit(1)
while 1:
try:
buf = fd.read(2048)
except socket.error, e:
print "Error receiving data: %s" % e
sys.exit(1)
if not len(buf):
break
sys.stdout.write(buf)
注意上面在我们发送了数据之后,使用了shutdown方法,是为了保证发送的数据成功到达目标机器。因为shutdown()会等待,直到接收到一个准确的退出代码。
服务器端(Network Server)
通过TCP创建一个服务端可以总结为如下四个步骤:1. 创建一个socket对象。(create socket object)
2. 设置socket对象的属性。(set options)
3. 绑定一个端口。(bind to a port)
4. 监听来自客户端的连接。(listen for connection)
针对上面的四个步骤,下面是一个最简单的实现:
host = '' #接受来自任何端口的连接
port = 51423
#第一步,创建一个socket对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#第二步,设置socket属性
s.setsockopt(socket.SOL_SOCKET, socket.SO_RESUSEADDR, 1)
#第三步,绑定一个端口
s.bind((host, port))
#第四步,监听来自客户端的连接
s.listen(5) #参数5表示同时监听5个连接
port = 51423
#第一步,创建一个socket对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#第二步,设置socket属性
s.setsockopt(socket.SOL_SOCKET, socket.SO_RESUSEADDR, 1)
#第三步,绑定一个端口
s.bind((host, port))
#第四步,监听来自客户端的连接
s.listen(5) #参数5表示同时监听5个连接
通过UDP创建一个服务端步骤也差不多,创建一个socket,设置option,bind端口,然而,UDP不需要listen()和accept(),而是使用recvfrom()就足够了。recvfrom()函数返回两个信息:接受的数据(data)和客户端的地址(address)和端口(port)。下面是UDP服务端的例子:
#!/usr/bin/env python
# UDP Echo Server - Chapter 3 - udpechoserver.py
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 Echo Server - Chapter 3 - udpechoserver.py
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()
Domain Name System(DNS)
我们能很轻松的记住博客园的域名,却基本上很难说出它的IP地址来,因为DNS为我们解析了域名。socket.getaddrinfo()根据主机名或域名等来获取相应的信息。
socket.getaddrinfo(host, port[, family[, socktype[, proto[, flags]]]]))
返回值是一个tuple的列表,每个tuple返回如下信息:
(family, socktype, proto, canonname, sockaddr)
同时,gethostbyaddr()根据IP地址获取相应的信息,同时使用getaddrinfo()和gethostbyaddr()可以实现对域名的双重验证。如下面的例子:
import sys, socket
def getipaddrs(hostname):
"""Get a list of IP addresses from a given hostname. This is a standard
(forward) lookup."""
result = socket.getaddrinfo(hostname, None, 0, socket.SOCK_STREAM)
return [x[4][0] for x in result]
def gethostname(ipaddr):
"""Get the hostname from a given IP address. This is a reverse
lookup."""
return socket.gethostbyaddr(ipaddr)[0]
try:
# First, do the reverse lookup and get the hostname.
hostname = gethostname(sys.argv[1]) # could raise socket.herror
# Now, do a forward lookup on the result from the earlier reverse
# lookup.
ipaddrs = getipaddrs(hostname) # could raise socket.gaierror
except socket.herror, e:
print "No host names available for %s; this may be normal." % sys.argv[1]
sys.exit(0)
except socket.gaierror, e:
print "Got hostname %s, but it could not be forward-resolved: %s" % \
(hostname, str(e))
sys.exit(1)
# If the forward lookup did not yield the original IP address anywhere,
# someone is playing tricks. Explain the situation and exit.
if not sys.argv[1] in ipaddrs:
print "Got hostname %s, but on forward lookup," % hostname
print "original IP %s did not appear in IP address list." % sys.argv[1]
sys.exit(1)
# Otherwise, show the validated hostname.
print "Validated hostname:", hostname
def getipaddrs(hostname):
"""Get a list of IP addresses from a given hostname. This is a standard
(forward) lookup."""
result = socket.getaddrinfo(hostname, None, 0, socket.SOCK_STREAM)
return [x[4][0] for x in result]
def gethostname(ipaddr):
"""Get the hostname from a given IP address. This is a reverse
lookup."""
return socket.gethostbyaddr(ipaddr)[0]
try:
# First, do the reverse lookup and get the hostname.
hostname = gethostname(sys.argv[1]) # could raise socket.herror
# Now, do a forward lookup on the result from the earlier reverse
# lookup.
ipaddrs = getipaddrs(hostname) # could raise socket.gaierror
except socket.herror, e:
print "No host names available for %s; this may be normal." % sys.argv[1]
sys.exit(0)
except socket.gaierror, e:
print "Got hostname %s, but it could not be forward-resolved: %s" % \
(hostname, str(e))
sys.exit(1)
# If the forward lookup did not yield the original IP address anywhere,
# someone is playing tricks. Explain the situation and exit.
if not sys.argv[1] in ipaddrs:
print "Got hostname %s, but on forward lookup," % hostname
print "original IP %s did not appear in IP address list." % sys.argv[1]
sys.exit(1)
# Otherwise, show the validated hostname.
print "Validated hostname:", hostname
OK,第一部分就到这里。
微信扫一扫交流
作者:CoderZh
公众号:hacker-thinking (一个程序员的思考)
独立博客:http://blog.coderzh.com
博客园博客将不再更新,请关注我的「微信公众号」或「独立博客」。
作为一个程序员,思考程序的每一行代码,思考生活的每一个细节,思考人生的每一种可能。
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。