应用层:UDP 套接字编程

UDP

UDP 是一种不提供不必要服务的轻量级运输协议,它仅提供最小服务。UDP 是无连接的,因此在两个进程通信前没有握手过程。UDP 协议提供一种不可靠数据传输服务,也就是说,当一个进程讲一个报文发送进 UDP 套接字时,UDP 协议并不保证该报文将到达接收进程。不仅如此,到达接收进程的报文也可能是乱序到达的。
UDP 没有包括拥挤控制机制,所以 UDP 的发送端可以用它选定的任何速率向其下层(网络层)注入数据。(然而,值得注意的是实际端到端吞吐量可能小于该速率,这可能是因为中间链路的带宽受限或因为拥塞而造成的。)——《Computer Networking A Top-down Approach》

  • 请思考:UDP 对比 TCP,有很多服务都不提供,那么为什么还需要 UDP?
    简单地说,由于 UDP 不需要握手,没有流量控制,因此可来以快速传输,在一些情景下可以体现出更高的效率。同时由于 UDP是不可靠的,因此就不需要重传,对于因特网电话而言就不会出现不同步的问题。

套接字编程(UDP)

对于一个网络应用程序,他们之间是如何实现信息交互的呢?对于一个典型的网络应用而言,需要有两个程序来共同完成,分别是客户端程序和服务器程序,这两个程序将分别位于两个端系统之中。当我运行这两个程序时,就会创建一个客户进程和服务器进程,这两个进程将会通过套接字的读取和写入,来实现端系统间的通信。
现在我们的想法是,编程实现一对应用程序——客户端应用程序员服务器应用程序,在这篇博客我们选择在 UDP 上运行,原理是从一个端系统向另一个端系统发送独立的数据分组,由于 UDP 是无连接的,因此发送分组时不对交互的内容提供任何保障。
通过学习,我们知道不同端系统的进程是通过彼此之间向套接字发送报文,通过这样的信息交互来实现通信,套接字就好比是门禁,想要和应用程序进行通信,需要先通过门禁的验证,是我的客人才允许入内。同理,也不是什么报文都能随意出门的,必须是得到允许的报文才会被送出门去!

首先我们先要把目的地址添加在一个分组上,这么做相当于分组得到许可了,那就可以通过套接字这个“门禁”出发。出发之后就把一切都交给因特网啦,因特网会通过一系列操作把分组送到接收进程的套接字上啦。接收端的套接字收到分组时,进程就会将分组通过套接字取回,进行信息的交互啦。
这个过程你可以想象成送信,你需要把一封信该有的信息,例如邮政编码、邮票、收件人信息等之类的东西搞搞好,然后交给邮递员啦,邮递员就会用他的方式想方设法把信送到收件人的门前啦。收件人并不是看到信就收了,而是先看看这封信是不是给自己的,是的话才拿进门里啦。

所以我们比较关心的问题是,我们需要把什么样的信息搞到分组上,才能实现这样的功能呢?为了连接主机,我们需要目标主机的 IP 地址,这样才能知道要发给哪个端系统,就想送信就一定要有收件人啦,没有收件人的话送信就是一件毫无意义的事情。但是由于一台主机上可能运行着好多个进程嘞,那我要送到哪个进程比较好呢?肯定不能乱送吧,所以我们需要指定一个端口号,这样就能从指定的地方,被指定的进程接收分组啦。需要强调的是,我们自己写的端口需要避开 RFC 定义的协议,就例如 HTTP 协议的端口号 80,因为我们如果从这些端口送分组的话会“串线”,这是我们不愿意看到的事情。

打招呼!(程序需求)

我们做个基础的玩意,让客户端和服务器互相打个招呼吧!

  1. 显示提示信息,客户从利用键盘输入自己的名字;
  2. 通过程序,将该数据向服务器发送;
  3. 服务器接收该数据并生成一段打招呼信息,例如 “Hello!”;
  4. 服务器将数据发回给客户;
  5. 客户接收数据并显示返回的数据。
  • 下文使用 Python 实现。

UDPClient

我们一步一步来,“巧妇难为无米之炊”,首先我需要一些工具来专门制造套接字,我们就没必要造轮子啦。在 Python 中有个模块是专门用来做这件事情的,名为 “socket” 我们先把它包括进来。

from socket import *

接下来我需要交代接收分组的目的主机,那就分别造 2 个变量来存储就行了。

Name = '名字'    #用字符串来存目的地址,“名字”需要替换为目的地的 IP 地址或域名 
Port = 端口号    #“端口号”需要替换为一个存在的端口,例如 12000,注意不要“串线”

好,接下来我们需要生成一个套接字来工作,直接看代码吧。不过我们现在看这段代码可能是一头雾水的,这是啥?别急,我们学到网络层的时候就会认识 “AF_INET” 和 “SOCK_DGRAM” 这两个东西了。简单地说,这段代码制造了一个 UDP 类型的套接字,我们直接先记下来就行。

Socket = socket(AF_INET,SOCK_DGRAM)

接下来我们就要像要求所说的那样,让客户输入他的名字。软件的交互性要好,我们也可以来点提示信息。

print("现在向服务器打个招呼吧!")
message = input("Nice to meet you! My name is ")

好了,现在我们既有了套接字,也有了报文,接下来就要把信息发送出去啦。首先我们先要把报文转换为字节类型,这里可以调用 “.encode()” 方法完成,然后我们让这个报文夹带上目的地址和端口号。
通过 “.sendto()” 方法就可以把分组送给套接字啦。接下来的操作是由套接字在内部自动完成的,我们不需要太操心。

Socket.sendto(message.encode(),(Name,Port))

到此为止,我们已经完成了发送分组的需求。不过还没有结束,我们还要接受服务器返回的信息,并做一些响应表示我们成功接受了。
看下面的这个语句,利用 “Socket.recvfrom()” 可以令套接字接受返回的信息,然后我们把这些信息赋给两个变量,分别存储返回的分组服务器的地址(其实这玩意我们暂时用不上)。至于 “2048” 是什么意思呢?它表示 “.recvfrom()” 方法使用缓存长度 2048 作为输入,不理解也没关系。

backdMessage,address = Socket.recvfrom(2048)

然后我们把分组输出来看看。

print(backMessage.decode())

还记得我们读取文件时,完成操作后要做什么吗?那就是关闭文件,这是一个好习惯。对于套接字也是如此,我们也要养成好习惯,一个进程使用完了之后就把它关闭掉。“.close()” 方法就能实现这个功能了。到此,程序编写结束!

Socket.close()

UDPServer

刚刚我们写的是客户端的 Python 程序,现在我们来写一下服务器端的程序。一步一步来,首先我们还是先把 “socket” 模块包括进来。

from socket import *

接下来我们要指定一下端口号,表示我服务器的套接字只接受某个端口传来的信息,体现一个“门当户对”!我们用一个变量来存下端口号,然后和上文一样,也得造一个套接字出来,我们通过 “.bind()” 方法把端口号绑定给这个套接字。

Port = 端口号    #“端口号”需要替换为一个存在的端口,例如 12000,注意不要“串线”
Socket = socket(AF_INET,SOCK_DGRAM)
Socket.bind(('',Port))

有了上述代码之后,服务器的套接字就能够从指定的端口号接收信息了,为了表示上述操作顺利实现,我们输出一个提示信息。

print("准备就绪,可以接收分组!")

作为一个服务器,我们肯定不是仅仅接收一次分组啦,肯定是不停地准备着相应送来的报文啦,也就是说我这个服务器是要持续保证在工作状态。我们使用一个死循环 “while true” 实现,注意这个时候死循环是不会太消耗资源的,因为接下来的语句之后读取到了分组之后才会有所行动。
若接收到了分组,“.recvfrom()” 方法就会开始工作,把数据和发送方的地址存起来。在这里因为我们要往回发分组,所以这里的发送方地址就很重要了,当我们编辑好回复的报文时,要把发送方的地址一起通过 “.sendto()” 方法提交给套接字。接下来就不用管了,交给套接字完成吧!

while True:
    message,Address = Socket.recvfrom(2048)
    backMessage = "Hello, " + message.decode().upper()+"! My name is Han Meimei!"
    Socket.sendto(backMessage.encode(),Address)

当然有小伙伴会问了,那这个死循环什么时候停止呢?那多简单,不想用的时候直接把程序关了就好了喽!

程序测试

由于我没有小伙伴陪我,我只好在一台机子做测试喽,先把服务端程序 “UDPServer.py” 启动起来。

看到提示信息,证明服务器启动很成功,接下来启动客户端程序。

看样子也挺顺利的,我们开始打招呼吧!

程序完成了我们的需求!

参考资料

《计算机网络》 谢希仁 编著,电子工业出版社
《计算机网络 自顶向下方法》 [美] James F.Kurose,Keith W.Ross 著,陈鸣 译,机械工业出版社
为什么很多网络协议都是UDP为主的呢?
tcp和upd的区别?为啥要用udp
为什么要有UDP协议?

posted @ 2020-04-20 01:37  乌漆WhiteMoon  阅读(482)  评论(0编辑  收藏  举报