IP层需要解决两个问题:
- 通过多路复用,区分不同应用程序的数据包
- 通过可靠传输,修复错误
两个主要协议:用户数据包协议(UDP)、传输控制协议(TCP),UDP用端口多路复用,解决问题一。TCP可以同时解决这两个问题。
UDP
不得不坦率的承认,我们不太可能在自己的任何一个应用程序中使用UDP,如果认为UDP适用于某个应用,不妨了解一下消息队列。
端口号
一共16位,0-65536之间
- 知名端口0-1023
- 注册端口1024-49151 有IANA推荐的端口,可能被占用
- 其余49152-65536 可以随意使用,端口池
套接字socket
- POSIX系统:可移植操作系统接口(Portable Operating System Interface), 比如Linux和Mac OS X
- 套接字(socket):一个通信端点,操作系统使用整数来标识。Python使用
socket.socket
对象创建。是一个IP地址和端口号组成的元组。
UDP丢包可能由于网络媒介,或者网络繁忙,重发请求应该采取指数退避。
请求ID是解决重复响应的好方法。重复响应问题是我们收到所有数据包后又收到一个被认为丢失的响应,此时可能误解这个是当前请求的响应。随机选择请求ID可以预防最简单的电子欺骗(spoofing)
区分服务端绑定bind()
和客户端连接connnec()
,绑定指定要使用的端口,而连接限制了客户端可以接受的响应。
运行在不同服务器上的UDP代码
argparse
是用来处理arg的包
#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter02/udp_remote.py
# UDP client and server for talking over the network
import argparse, random, socket, sys
MAX_BYTES = 65535
def server(interface, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((interface, port))
print('Listening at', sock.getsockname())
while True:
data, address = sock.recvfrom(MAX_BYTES)
if random.random() < 0.5:
print('Pretending to drop packet from {}'.format(address))
continue
text = data.decode('ascii')
print('The client at {} says {!r}'.format(address, text))
message = 'Your data was {} bytes long'.format(len(data))
sock.sendto(message.encode('ascii'), address)
def client(hostname, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect((hostname, port))
print('Client socket name is {}'.format(sock.getsockname()))
delay = 0.1 # seconds
text = 'This is another message'
data = text.encode('ascii')
while True:
sock.send(data)
print('Waiting up to {} seconds for a reply'.format(delay))
sock.settimeout(delay)
try:
data = sock.recv(MAX_BYTES)
except socket.timeout as exc:
delay *= 2 # wait even longer for the next request
if delay > 2.0:
raise RuntimeError('I think the server is down') from exc
else:
break # we are done, and can stop looping
print('The server says {!r}'.format(data.decode('ascii')))
if __name__ == '__main__':
choices = {'client': client, 'server': server}
parser = argparse.ArgumentParser(description='Send and receive UDP,'
' pretending packets are often dropped')
parser.add_argument('role', choices=choices, help='which role to take')
parser.add_argument('host', help='interface the server listens at;'
' host the client sends to')
parser.add_argument('-p', metavar='PORT', type=int, default=1060,
help='UDP port (default 1060)')
args = parser.parse_args()
function = choices[args.role]
function(args.host, args.p)
绑定接口
服务器bind()
绑定接口时,可以使用:
-
127.0.0.1
本机上其他应用程序接口 - 空字符串或者
0.0.0.0
该服务器的任意网络接口 - 该服务器的一个外网IP
- 客户端是同一台机器的自环接口
127.0.0.1
,不会响应 - 客户端是本机外网IP,可以连通
- 客户端是其他机器,可以连接
- 客户端是同一台机器的自环接口
UDP分组
MTU:最大传输单元、最大数据包容量,以太网和无线网卡一般MTU为1500B
较大数据包会被分解,可以查看系统MTU
// import IN 已经被py3.6弃用了。。更新为手动写
class IN:
IP_MTU = 14
IP_MTU_DISCOVER = 10
IP_PMTUDISC_DO = 2
sock.setsockopt(socket.IPPRPTO_IP, IN.IP_MTU_DISCOVER, IN.IP_PMTUDISC_DO)
套接字选项
getsockopt()
和setsockopt()
来获取和设置,第一个参数为所属选项组,第二个参数为要设置的选项名
比如设置广播:
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
其他设置是否经过网关SO_DONTROUTE
,判断套接字类型(UDP/TCP)SO_TYPE
广播
设置允许广播后,还需要客户端目标地址改为广播地址,用ifconfig查看,或者使用python中的特殊广播地址符
, 如果使用普通地址,效果和未设置广播一样。
何时使用UDP
- 历史原因
- 对时间要求严格的媒体流
- 设计使用LAN子网的多播
当我们发现有一种UDP协议能够适用于应用程序时,我们很可能已经重新实现了TCP。不过,实现的很糟糕。