目录
1 UDP Introduction
Network的Transport Layer需要提供两种服务:
-
Multiplexing(多工)和Demultiplexing(分工),即可以在一个IP上有多个数据传输通道,这通过Port Number来解决。
-
可靠的传输,即Packet的lost,out of order,duplication不会发生,在server发送的数据原封不动的到client。
那么什么是UDP和TCP呢?最简短的答案如下。
UDP:IP+Port
TCP:IP+Port+Reliability
2 UDP Programming
UDP和TCP的编程我们都要用到Python的socket
模块,那么什么socket呢?
Socket:从Network Protocol Stack来说,Socket是Application Layer和Transport Layer之间的接口。从计算机系统来说,Socket是一个特殊的文件。socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket是该模式的一个实现。
python的socket模块分四部分,包括顶层方法,socket类,常量和异常。其中socket类里分成构造器,关闭和read/write三部分。
2.1 基本UDP:杂乱客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import argparse,socket
from datetime import datetime
MAX_BYTES = 65535
def server(port):
sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #创建UDP socket
sock.bind(('127.0.0.1',port)) #服务器bind地址(IP,port)
print('Listening at {}'.format(sock.getsockname()))
while True: #一直监听
data, address = sock.recvfrom(MAX_BYTES) #接收数据
text = data.decode('utf-8')
print('The client at {} says{!r}'.format(address,text))
text = 'Your data is {} bytes long'.format(len(text))
data = text.encode('utf-8')
sock.sendto(data,address) #发送数据
def client(port):
sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #创建UDP socket
text = 'The time is {}'.format(datetime.now())
data = text.encode('utf-8')
sock.sendto(data,('127.0.0.1',port)) #发送数据
print('The OS assigned me the address {}'.format(sock.gethostname()))
data, address = sock.recvfrom(MAX_BYTES) # Dangerous! #接收数据
text = data.decode('utf-8')
print('The server {} replied {!r}'.format(address,text))
if __name__ == '__main__':
choices = {'client':client,'server':server}
parser = argparse.ArgumentParser(description='Send and receive UDP locally')
parser.add_argument('role',choices=choices,help='which role to play')
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.p)
这个例子为了code更紧凑,通过argparse
模块将client和server放在一起,通过命令端来执行。服务端和客户端UDP的主要区别是服务端的address必须已知,这通过sock.bind(('127.0.0.1',port))
来指定,port通过命令行输入,默认是1060。这个例子有一个明显的漏洞,就是客户端可以接受任何发送过来的数据,即使不是我们的服务端,这就会造成网络漏洞。这种naive的漏洞并不是黑客通过非常手段来攻击,而是开发者不够专业导致。
你可以开启一个服务端,然后ctrl+z
suspend它;然后创建1个客户端,发送信息给服务端。由于服务端被挂起,因此不会收到信息回复给客户端。客户端这个时候就在等回复。现在尝试开启一个新的socket,给该客户端发信息,你会发现客户端是可以收到的。这个过程就模拟了真实网络世界的上面这个网络漏洞。这样的客户端技术上被称为“杂乱客户端(Promiscuous Client)”。
那么如何解决该问题呢,下面提供两种方法。
2.2 进阶UDP:非杂乱客户端
2.2.1 Connecting UDP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import argparse,socket
from datetime import datetime
import random
MAX_BYTES = 65535
def server(interface, port):
sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
sock.bind((interface,port))
print('Listening at {}'.format(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('utf-8')
print('The client at {} says{!r}'.format(address,text))
text = 'Your data is {} bytes long'.format(len(text))
data = text.encode('utf-8')
sock.sendto(data,address)
def client(hostname, port):
sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
sock.connect((hostname,port)) # 客户端connect到服务端
text = 'The time is {}'.format(datetime.now())
data = text.encode('utf-8')
print('The OS assigned me the address {}'.format(sock.getsockname()))
delay = 0.1;
while True:
sock.send(data)
sock.settimeout(delay)
try:
data= sock.recv(MAX_BYTES) # 只能接收该服务端发回来的信息
except socket.timeout as e:
delay *=2
print('delay time: {}'.format(delay))
if delay >=2:
raise RuntimeError("I think the server is down")
else:
break
text = data.decode('utf-8')
print('The server replied {!r}'.format(text))
if __name__ == '__main__':
choices = {'client':client,'server':server}
parser = argparse.ArgumentParser(description='Send and receive UDP locally,'
'pretending packets are often dropped')
parser.add_argument('role',choices=choices,help='which role to play')
parser.add_argument('hostname',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.hostname,args.p)
这个时候, 你如果再开启服务端和客户端, 将服务端suspend, 伪造一个服务端给该客户端发送信息, 该客户端是收不到的。 需要注意的是在connect()
后,sendto()
和recvfrom()
分别改成send
和recv
, 因为系统已经通过connect()
设置了服务端地址。
在server(interface,port)
方法里第一个参数我们命名为interface
而在client(host,port)
方法里第一个参数我们命名为host
是有原因的。 因为server的IP地址是服务端IP地址, 可以是localhost或者external IP,两者统一为’0.0.0.0’,这称为interface
。而client的第一个参数是host的IP,因此命名为host
。
细心的读者已经发现了,客户端还增加了一个循环,里面将timeout进行指数化改变,直到收到数据或者timeout超过最大时限才推出循环,这称之为exponential backoff。exponential backoff使得timeout后的request越来越不频繁,利于网络恢复。
2.2.2 Requests IDs
第二种防止“杂乱客户端(Promiscuous Client)”的方法是记录ID。只有当返回的Packet里的ID符合客户端发出去的ID客户端才接受。对于ID的选择需要在一个大范围内随机生成,防止攻击者轻易猜出或者发N个Packet穷举ID。但是如果攻击者可以看到客户端Packet里的ID,这种方法就不适用了,这个时候就要用到加密,这我们会在第6篇笔记中介绍。
2.2.3 其他
2.2.3.1 IP Interface
对于IP,一台电脑有两个:
-
本地IP‘127.0.0.1’,域名是localhost;
-
外部IP‘175.159.187.176’,网络分配的。
两者统一为‘0.0.0.0’。也就是说如果服务端地址是‘0.0.0.0’,那么客户端发给‘127.0.0.1’和‘175.159.187.176’的信息都会被服务端收到。
2.2.3.2 UDP Framentation
当UDP的Packet的大小超过MTU时,会被分成几个小的Packet。如果小的Packet个数越多,整个文件就越有可能传输失败(因为任何一个Packet的丢失都会导致整个文件不完整)。下面是一段检测MTU的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import IN, argparse,socket
MAX_BYTES = 65535
if not hasattr(IN,'IP_MTU'):
raise RuntimeError('cannot perform MTU discovery on this combination'
' of operating system and Python distribution')
def send_big_datgagram(host,port):
sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
sock.setsockopt((socket.IPPROTO_IP,IN.IP_MTU_DISCOVER,IN.IP_PMTUDISC_DO))
sock.connect((host,port))
try:
sock.send(b'#' * 65000)
except socket.error:
print('Alas, the datagram did not make it')
max_mtu = sock.getsockopt(socket.IPPROTO_IP,IN.IP_MTU)
print('Actual MTU: {}'.format((max_mtu)))
else:
print('The big datagram was sent!')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Send UDP packet to get MTU')
parser.add_argument('host',help='the host to which to target the packet')
parser.add_argument('-p',metavar='PORT',type = int, default = 1060,help = 'UDP port (default 1060)')
args = parser.parse_args()
send_big_datgagram(args.host,args.p)
2.2.3.3 Socket Options
socket可以设置各种选项来定制。
2.2.3.4 Broadcast
如果说UDP有一个特殊的能力的话,那就非Broadcast(广播)功能不可了。在局域网内不同主机上开启多个服务端,监听信息;让客户端往’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import argparse,socket
MAX_BYTES = 65535
def server(interface, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((interface, port))
print('Listening for datagrams at {}'.format(sock.getsockname()))
while True:
data, address = sock.recvfrom(MAX_BYTES)
text = data.decode('ascii')
print('{}: the client at {} says: {!r}'.format((interface,port),address, text))
def client(network, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
text = 'Broadcast datagram!'
sock.sendto(text.encode('ascii'), (network, port))
if __name__ == '__main__':
choices = {'client': client, 'server': server}
parser = argparse.ArgumentParser(description='Send, receive UDP broadcast')
parser.add_argument('role', choices=choices, help='which role to take')
parser.add_argument('host', help='interface the server listens at;'
' network 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)
2.3 何时适用UDP
只有下面三种情形在你的职业生涯中会用到UDP:
-
你想实现或者改进一个已经存在的基于UDP的protocol;
-
实现实时的语音或者视频protocol;
-
局域网的广播。