目录


1 UDP Introduction

Network的Transport Layer需要提供两种服务:

  1. Multiplexing(多工)和Demultiplexing(分工),即可以在一个IP上有多个数据传输通道,这通过Port Number来解决。

  2. 可靠的传输,即Packetlost,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来说,SocketApplication LayerTransport Layer之间的接口。从计算机系统来说,Socket是一个特殊的文件。socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket是该模式的一个实现。

python的socket模块分四部分,包括顶层方法,socket类,常量和异常。其中socket类里分成构造器,关闭和read/write三部分。

18.1_socket

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+zsuspend它;然后创建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()分别改成sendrecv, 因为系统已经通过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 backoffexponential 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,一台电脑有两个:

  1. 本地IP‘127.0.0.1’,域名是localhost;

  2. 外部IP‘175.159.187.176’,网络分配的。

两者统一为‘0.0.0.0’。也就是说如果服务端地址是‘0.0.0.0’,那么客户端发给‘127.0.0.1’和‘175.159.187.176’的信息都会被服务端收到。

Network Interface

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(广播)功能不可了。在局域网内不同主机上开启多个服务端,监听信息;让客户端往’'这个IP发信息(端口号多个服务器一致)。则可以看到所有服务端都有响应。这就是**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:

  1. 你想实现或者改进一个已经存在的基于UDP的protocol;

  2. 实现实时的语音或者视频protocol;

  3. 局域网的广播。

3 总结

Chapter 2 summary

4 参考资料


Share Post

Twitter Google+

Shunmian

The only programmers in a position to see all the differences in power between the various languages are those who understand the most powerful one.