Browsed by
Category: 通信技术

通信,让生活更美好!

奇怪的 SIP 呼叫流程

奇怪的 SIP 呼叫流程

最近几天帮助泰国的朋友检查一个呼叫业务流程,其中涉及很多细节和业务流程,不过让我觉得特别意外的是他使用的 VoIP 运营商的 SIP 呼叫流程。首次遇到这样的流程,请先参考下图的概要描述:

奇怪的 SIP 呼叫流程
奇怪的 SIP 呼叫流程

这个流程的奇怪之处在于:(1)VoIP 运营商发起呼叫时,INVITE 消息的媒体地址居然是“0.0.0.0”,这很明确告诉被叫:主叫只发送媒体流、甚至根本不处理媒体流;(2)被叫应答后,VoIP 运营商再次发起 reINVITE 流程,此时才真正指示出自己的真实媒体地址(当然也包括最终的媒体编码)。

通常的 SIP 呼叫流程在发起呼叫时就明确指示自己的真实媒体信息,因此在被叫应答后,没有必要再发起 reINVITE 流程。然而这家运营商为什么要放弃传统做法、采用这么奇怪的流程呢?

这家运营商是美国的一家运营商,而且规模很大,其设备供应商也是一家世界级的大软件公司(非常、非常大),因此这个流程肯定不是随意修改的结果,必定有其特殊的目的。仔细揣摩后,我认为它可能是为了节省媒体资源才这么做。

被叫方一般会振铃几秒、十几秒、甚至几十秒后,才可能应答。另外,呼叫量特别大时,统计上只有10%左右的呼叫最终才会应答。这家运营商采用这个流程,只需要在被叫真正应答之后才开始分配媒体资源,考虑到这是一家规模很大的运营商,这种流程确实可以节省很多的媒体资源。

VoIP 网络往往是主叫播放回铃音,因此这是个非常聪明、折中的呼叫流程,确实只有在被叫侧应答之后才处理真实媒体流。然而现实网络组网是非常复杂的,这个聪明到有些投机取巧的方法有固有的缺陷,请参考下图:

183 带媒体信息
被叫放音,183 消息携带 SDP。

被叫侧如果对接了传统的 PSTN 网络,例如 VoIP 网关,很有可能直接返回 183 消息并携带被叫的媒体信息。传统的 PSTN 网络往往是被叫侧播放回铃音,并且也有一些被叫侧放音的业务(例如“彩铃”),此时这家 VoIP 运营商就有问题了。

由于它告诉给被叫的地址是“0.0.0.0”,因此被叫振铃音(或者业务音)就无法传达到主叫侧。VoIP 运营商可以向被叫发送 UPDATE 消息来传递自己的真实媒体信息,但被叫未必会支持 UPDATE 消息,这个是扩展消息,不是所有的 SIP 设备都必须支持。而且 PSTN 网络往往会要求对 18x 消息用 PRACK 流程确认,因此即便支持 UPDATE 消息,这个特殊流程实际也急剧放大了被叫侧振铃阶段流程的复杂度。

这也就是我们的泰国朋友遇到的问题。

解决方式是在 VoIP 运营商与 PSTN 网络之间架设 miniSIPServer:

(1)miniSIPServer 与 VoIP 之间建立完全应答的呼叫通道;

(2)miniSIPServer 与 PSTN 之间维持正常的呼叫流程;

(3)miniSIPServer 判断被叫侧是否自己放回铃音(或者业务音),如果(a)被叫没有放音,则 miniSIPServer 主动给 VoIP 运营商放回铃音(或者呼叫等待音),而如果(b)被叫有放音,miniSIPServer 则直接建立两边的通道,让主叫直接听被叫的放音。

NAME、CNAME 格式

NAME、CNAME 格式

总体而言, 早期 DNS 协议是个比较简单的协议。受限于 UDP 大小和数据量, 基于 UDP 的 DNS 协议限制了包大小,并且采用二进制编码方式力求减少数据量占用。

二进制编码方式相比文本方式(例如HTTP、SIP、SMTP等)最明显的缺点就是不直观,再用一些技巧减少数据量,DNS 协议在定义、实现方面总显得有些弯弯绕绕。

比如 NAME、CNAME 等结构的编码。

本文记录了这些古怪的编码方式,方便以后查找,并帮助后续的开发者不会被代码给绕晕。作为我个人来说,还是尽快忘记这些吧。


方式一:纯压缩编码方式

这种方式应用很普遍,以“0xc0”标记开始,加上一个字节的 offset,进行消息包内的寻址即可。如下图所示:

DNS offset 编码
offset 方式编码

其中, 0x0c 就是偏移量。


方式二:直接编码方式

这种方式比较直接,占用数据量也相对大一些,将域名完整信息存在数据包中。如下图所示:

直接编码
直接编码方式

需要说明的是:并不是直接将所有字符存进去就 OK 了。对于域名数据,以“.”符号分割成几个单元,每个单元数据采用 “length + value” 方式进行编码,最后以0x00结束编码。以上述数据为例:

08 6e 73 69 6e 61 69 6d 67 04 67 73 6c 62 08 73
69 6e 61 65 64 67 65 03 63 6f 6d 00

比如,第一个 0x08,说明后面的“nsinaimg”(即 6e 73 69 6e 61 69 6d 67)长度;然后是 0x04,说明后面的“gslb”(即67 73 6c 62)长度……其他单元类推。


方式三: 混合编码方式

这种方式其实就是综合了上述两种方式。以直接编码方式开始,然后以压缩(偏移)编码方式结尾。如下图所示:

混合编码方式
混合编码方式

对图中的数据码流描述如下:

03 6b 6c 6e 04 67 72 69 64 c0 38

0x03 描述了“kln”(即 6b 6c 6e)的长度,0x04 描述了“grid”(即 67 72 69 64)的长度,而域名的后续部分,通过“c0 38”编码可以知道:需要偏移0x38字节获取。

RFC4235与RFC7463

RFC4235与RFC7463

基本上两篇RFC文档是传承的关系,RFC7463场景应用比RFC4235要详细很多,一些旧的SIP设备未必支持7463。

在兼容性方面,对于SIP-SUBSCRIBE消息,两篇RFC文档采用了不同的Event,rfc4235中定义“dialog”,而rfc7463中定义“dialog;shared”。rfc4235限定在只订阅SIP呼叫对话的状态,因此在dialog-info中,要求必须填充dialog元素,其中就包含call-id,remote-tag以及local-tag等典型呼叫参数。

而rfc7463不仅仅是关注呼叫,更关注“状态呈现”,因此凡是与“状态”相关的消息,都尽量进行了定义。比如在“11.1. Registration and Subscription”章节中,就定义了终端注册的状态呈现。在注册流程中,就没有dialog的信息。

呈现信息多,对用户当然有好处,对VoIP系统也很有意义,尤其是在企业应用场景中。但是需要注意的是这也大大增加了VoIP系统的负荷,实际部署中要慎重考虑。

3cx收购了elastix

3cx收购了elastix

原始新闻链接请点击此处。从新闻内容大致可以得出以下结论:

(1)Elastix V5的内核从Asterisk迁移到3CX。考虑到3cx是商业、不开源的版本,因此elastix从V5版本开始应该就会转向闭源。

(2)以前的Elastix版本,例如使用比较广泛的V2版本,以及充满了bug的V3和V4版本,依然遵循开源协议。相信这些旧版本的用户无法免费升级到V5版本,需要付费购买产品或者购买技术支持才有可能。

(3)3CX可能看重的就是elastix目前已有的大量用户,希望能从产品技术支持、付费购买等途径获得利益。

国内一些专业QQ群里有部分用户表达了对这件事的愤怒情绪,感觉自己被Elastix背叛了。仔细搜索了相关报道和各类消息后,我有些不同的看法。对Elastix项目而言,『被收购』是个不错的商业选项,项目人员获得合理的报酬完全是可以理解的,毕竟这是个商业社会,人不能光靠理想一穷二白地活着。

相反,我对3cx的收购决定感到困惑。Elastix是个不错的项目,也有很多拥趸,但究其根本,依然只是依附在Asterisk上做了个壳而已,这也就意味着客户的迁移成本实际上并不高,客户完全可以从一个壳转另一个壳,比如FreePBX。实际上很快就发现FreePBX在twitter等社交媒体上发帖欢迎Elastix用户进行迁移。

由于这些壳都是使用Asterisk作为核心,因此配置规范(接口)、管理接口实际都遵循Asterisk定义。从Elastix转换到FreePBX,相信比转换到3cx要简单、容易得多。从这点看,我很怀疑Elastix用户会如3cx所愿地迁移到3cx的平台。如果不能顺利完成这种转换,这次收购的价值就会大打折扣。

目前市面上有各种各样的软、硬件PBX,绝大部分其实都是基于两个开源项目:Asterisk和FreeSwitch,差异化无非体现在各种定制化的包装。如果想获取最大利益,应该是直接收购或者控制这两大项目。如果说“条条大路通罗马”,那Elastix、FreePBX不过是其中两条路而已,Asterisk/FreeSwitch才是目的地罗马。与其在道路上设卡收费,不如直接控制罗马。

从这个角度考虑,所有基于这些开源项目的软、硬件产品实际上都有风险。一旦项目被收购,产品就有被扼杀的危险。避免此类危险的唯一路径,大概就是控制住这些项目,或者另起炉灶。

幸运的是我们的产品(miniSIPServer)是完全自主开发,与这些项目毫无关系,因此不存在任何法律和潜在的风险,这也使得我们能够完全掌握自己产品的发展,从而为客户提供安全、可靠、一致、长久的体验。

希望这些开源项目能长久繁荣下去,毕竟一起做大蛋糕、做大市场对大家都有利。

量子通信卫星

量子通信卫星

据说今天晚上要发射一颗量子通信卫星,顾名思义就是进行量子通信。对此我感到非常震惊!作为一名科普爱好者,我有限的量子理论知识大概只有以下几点:

(1)量子纠缠态。一个量子分裂成两个量子,这两个量子就是自纠缠的。一个向左转,另一个就向右转。理论上讲,这种纠缠态是跨越空间限制的,即使两个量子分布在宇宙的两端,它们的纠缠状态也不会改变。我猜想量子通信大概就是基于量子纠缠态来通信,通过控制一个量子的状态,影响另一个量子,从而实现通信。从这个角度讲,量子通信简直就是超越光速的通信方式。

(2)测不准原理。这基本就是现代量子理论的基石之一,大概是指无法准确测量量子的状态,因为测量本身就是一种能量干扰,会影响到量子的状态。如果测量某个参数越准确,则其他参数的结果就越不准确。

既然测不准,因此实际上是无法准确控制量子的状态。既然无法准确控制,就不可能实现通信。反过来说,如果实现了通信,则必然有某种手段能准确控制量子的状态,也就是说“测不准原理”是不是可以over了?

理论基石居然有可能被动摇,这简直就是突破天际!怎么做到的?感觉有幸要见证历史,求科普。

2016-08-19 更新:看了一些新闻报道,貌似是利用光子的不可分割、不可复制特点,产生密钥,对通信进行加密。当然这仍然很厉害,但是完全不是『利用量子纠缠态进行远距离通信』。我觉得『量子通信』这个表达不准确,应该是『量子加密』才对,天际并没有被突破。

解决DNS污染

解决DNS污染

DNS污染可能是一个比较普遍的问题,主要表现在:(1)DNS查询返回的IP地址根本是错误的。“错误”意思是IP地址要么根本不存在,要么就是个和域名完全无关系的地址。(2)IP地址虽然可能是对的,但是其他参数被莫名修改,例如TTL参数等。

测试了各类DNS服务器,例如阿里DNS、DNSPod(鹅厂)、Google DNS等。无一例外,最终的结果都或多或少被一些看不见的手给修改、甚至屏蔽了。从技术角度上讲,DNS协议本身非常随意、非常粗糙,是典型的互联网蛮荒时代的产物,比如面向UDP、无加密、无鉴权等,因此网络上任何一只手都有可能修改DNS的查询结果。网络进化到IPv6阶段,当然能解决这个问题,不过既然目前绝大部分设备还只支持IPv4协议栈,因此我们还是不得不修修补补来解决这个污染问题。

最根本的解决方法就是加密DNS查询。目前有些DNS服务商提供了私有的加密DNS方式,不过不太通用,需要私有的客户端程序配合。实际上可以不用搞这么复杂,自己建立加密通道,传递DNS消息即可。例如,参考下图的拓扑逻辑:

实现简单DNS透传的逻辑单元
实现简单DNS透传的逻辑单元

在这个网络中,有两个关键程序:dnsProxy和dnsAgent。

dnsProxy顾名思义就是个Proxy,本身并不负责DNS协议的解析,也不保存DNS的查询结果等信息,只是单纯地将DNS消息传递给真正的DNS服务器,并返回相应的结果即可。dnsProxy另一个功能是对外提供加密的数据连接,例如TLS、SSL加密等,甚至可以只是简单地对数据包进行自定义的异或运算即可。另外就是对外提供非标准连接接口,这点非常重要。DNS采用标准UDP53接口作为DNS服务器接口,网络上那些看不见的手,往往就是扫描并篡改53接口的数据包。这个小程序跑在境外(大家都懂的)的一台VPS设备上,推荐采用DigitalOcean,专业的云计算服务商,采用SSD硬盘,价格公道,我们一直用TA,如果你有兴趣的话,请点击这里自行了解细节。

dnsAgent是另一个小程序,主要负责建立与dnsProxy的加密连接,接收普通设备的DNS请求并将其传递给dnsProxy,同时返回DNS结果给普通计算设备。对于网内设备而言,dnsAgent就是个伪装的DNS服务器。同样,dnsAgent其实也不需要关心、也不需要解析DNS协议细节。在我的网络中,dnsAgent跑在一台常年吃灰的树莓派上(还是第一代的)。

实现这些仅仅需要一点UDP、TCP的网络知识,甚至不需要了解DNS协议的细节,无需对DNS数据包做修改。完成后可以愉快地打开很多以前打不开的网站。当然,有些网站始终是打不开的,这是另一个与DNS无关的话题了。

Pi打开IPv6

Pi打开IPv6

默认已经安装了IPv6协议栈,但是没有打开IPv6功能,这点比较奇怪。不过打开IPv6功能也不复杂,一行命令即可:

sudo modprobe ipv6
6to4 tunnel

6to4 tunnel

今天无意中发现,居然不用翻墙就能打开google和gmail,这简直太不科学了,我朝气象从此焕然一新?

本着“怀着最大的恶意揣测他人”的精神,我对此充满疑虑。于是拉出wireshark仔细dig了一下,发现了IPv6数据包,竟然是通过IPv6访问google和gmail!墙没有封锁IPv6,这实在是个惊奇的发现。

难道电信已经开始部署IPv6网络了吗?对此同样充满疑虑。但是有一点是肯定的:最近买的路由器是支持IPv6的。进入路由器管理界面,发现电信仍然是分配IPv4地址,但是在路由器IPv6设置项中,配置了“自动检测IPv6类型”,检测结果是“6to4 tunnel”,而且配置了IPv6地址。

看起来电信的确升级了网络,但是仍然没有真正部署IPv6,还在半遮半掩中,并且猥琐地限速了。有“6to4 tunnel”也还好,至少能访问外部IPv6的服务器地址。google和gmail等都部署了IPv6,因此可以通过IPv6来访问这些服务。而对于没有部署IPv6的服务,例如twitter之流等,依然还是无法访问。

不管怎样,这已经是向前迈进了一步。真心希望步子能迈得更大些!

2016-03-10 更新:好吧,我承认我还是图样了。

为什么放弃了webRTC?

为什么放弃了webRTC?

首先必须要说明的是:webRTC是非常好的技术,以至于我现在仍然在怀疑,放弃webRTC是不是个明智的决定,内心依然是忐忑不安。

然而现状就是将webRTC从MSS新版本中砍掉了。下面试图说明清楚做决定时的一些考虑。

从webRTC技术的发展脉络看,感觉ta更适合于公共网络的通信,尤其是越来越像专为google hangouts服务。由于是服务于公共网络,因此“加密”提高到一个非常重要的位置,几乎达到了神经质的地步。webRTC设计之初就没有考虑过传统的VoIP网络(尽管该技术来自于收购的GIPS团队,而该团队本身就是为VoIP网络开发出身的),从传输到业务,基本都特立独行。当然,这没什么不好,只是由于与传统VoIP网络切割开来,实在让人经常感到惋惜。

既然没有考虑传统VoIP网络,当然就更不可能考虑网络部署的多样性。全方位加密固然让自己显得安全,带来心理上的安慰,但也增加了网络部署的复杂性(VoIP本身已经够复杂了)。就一般的VoIP应用而言,“salt+MD5”以及SRTP已经足够保证通信的安全性和私密性。而最新的webRTC(不特别说明的话,这里我们总是指Chrome的webRTC实现)要求getUserMedia等操作必须是来自加密(HTTPS)的网站,websocket也必须进行加密。这也就是说,用户必须要为webRTC服务器(通常也是呼叫服务器或者IPPBX)部署单独的签名加密(TLS或者SSL)。对于提供公众服务的网站而言,部署TLS/SSL不是太大问题,而要求中小企业申请签名并部署在呼叫服务器中,这毫无疑问极大地增加了系统的复杂度,增加了部署难度,而且这样做的意义又有多大了?在企业通信网络里与其这样增加复杂度(网络复杂度和管理复杂度),还不如干脆直接建立VPN网络,方案很成熟,加密更全面,保护更周到,方式方法也简单得多。

这就是我们感觉webRTC只适合公众网络并且已经神经质的原因之一。何况,“加密”有时候更多的是个管理问题而不是技术问题。

webRTC另一个宏大的目标是提供各平台统一的用户体验。愿望是很美好的,但是是否能实现值得商榷。把所有的处理都放入浏览器,固然增加了可移植性,但最大的问题也就是牺牲了平台的独特性和高效性。比如在移动平台,webRTC的耗电量就明显偏高。而且在移动平台各种莫名其妙的设定,极大地损害了用户体验,比如播放ring-back tone,居然要求用户必须点击一个按钮才能继续(是我out了么?),这实在是太扯了。

而最被我们诟病的是兼容性。webRTC基本没有什么兼容性可言。按道理已经发布这么久了,多少应该在一些关键点上考虑一下兼容性,然而并没有。这对一个像google hangouts这样的网站来说,不是大问题,用户无非升级一下chrome好了。而对部署在许许多多用户voip网络中的PBX或者webRTC服务器而言,就是噩梦了。用户升级了Chrome,就必须同时升级服务器,如果不升级服务器,就必须降级Chrome。与之相比,microsoft对兼容性的考虑简直贴心贴肺。

这种对兼容性的唾弃有时候甚至直接表现在产品本身。比如当初的Gtalk,直接就放弃了,我们花费大量时间精力对接Gtalk,最后也是然并卵。对webRTC也会产生同样的顾虑。

webRTC当然有很多非常优秀的技术特点,比如源自GIPS的回音消除技术、超强的语音编解码技术等。思虑再三,决定还是不再跟随,砍了算了,以后再说吧。

实现一个简单的SMTP发送库

实现一个简单的SMTP发送库

最近在优化“语音邮箱”业务时,发现采用python-smtplib库虽然能满足需要,但是还是有很多不足,因此决定自己重新写一个库,当然仅仅实现发送email(包含附件)即可。实现一个完整的SMTP服务可能比较复杂,而实现一个单纯的、高性能的SMTP客户端发送库却比较容易。

与SIP、HTTP等协议类似,SMTP也采用文本方式定义消息格式,而附件等二进制内容,则采用base64转码成文本,这样就极为方便开发和调试。以下是SMTP最基础的几个RFC文档:

RFC5321: SMTP
RFC5322: Internet message format
RFC4954: SMTP Service Extension for Authentication
RFC3207: SMTP Service Extension for Secure SMTP over Transport Layer Security
RFC2035: Multipurpose Internet Mail Extensions

我们结合SMTP的流程,来简要说明SMTP协议中的各个关键点。

一、标准端口

SMTP基于TCP连接,SMTP服务器默认打开的端口是25、587或者465。通常是采用25端口,后期设计加密SMTP(即SMTPS)时采用了465端口。而IANA在新业务端口规划时占用了465端口,因此将587端口定义为TLS加密的SMTP端口。

目前各email服务商通常都会同时打开25和587端口,有些为了和以前老客户端程序兼容,也会打开465端口。

这里面其实可能存在一个误解,似乎25端口就是不加密的SMTP,而587、465就是加密的SMTP。实际上不是这样,现代SMTP服务器一般都已经支持STARTTLS(即RFC3207),因此在25端口也可以启动SSL/TLS加密,这取决于客户端是否具备SSL/TLS加密、解密能力,这点在后续流程中会详细描述。

二、基础流程

2.1 建立与SMTP服务器的连接

建立TCP连接,连接到服务器的25端口或者587端口。如果是25端口,则是建立普通TCP连接即可。如果是587端口,通常可以直接建立SSL/TLS连接。需要注意的是,有些SMTP服务器的587端口也是要求普通TCP连接,后续STARTTLS流程中再启动SSL/TLS加密。这种情况下,587端口的流程与25端口的流程一致。

我们的示例流程中,采用QQMail的SMTP服务器做参考,因此我们首先向“smtp.qq.com”服务器的25端口建立连接。连接成功后,服务器应返回220消息,告诉客户端连接成功:

220 smtp.qq.com Esmtp QQ Mail Server

2.2 初次握手

SMTP客户端发送EHLO命令给服务器,进行初次握手协商:

EHLO 192.168.1.101

“EHLO”实际是ESMTP(Extended SMTP)的指令,意思是要进行用户名和密码的鉴权。远古时代的SMTP不需要鉴权的情况下,可以直接发送HELO命令进行握手。目前的SMTP服务器基本都是支持ESMTP,因此如果不刻意区分的话,后续的SMTP实际就是指ESMTP。

SMTP服务器器收到上述消息后,返回本服务器的一些能力信息给客户端:

250-smtp.qq.com
250-PIPELINING
250-SIZE 73400320
250-STARTTLS
250-AUTH LOGIN PLAIN
250-AUTH=LOGIN
250-MAILCOMPRESS
250 8BITMIME

其中最关键的就是STARTTLS能力,也就是该SMTP服务器具备加密连接能力。通常都建议采用加密连接以确保数据安全,毕竟互联网上总有一些黑暗势力在窥探您的信息。

如果客户端不具备SSL/TLS能力,则直接跳到“账户鉴权”流程即可。此时就不能采用PLAIN鉴权模式,必须采用MD5等加密鉴权模式。

2.3 加密连接

既然服务器明确了SSL/TLS能力,因此客户端只要启动SSL/TLS加密TCP连接即可。客户端发送STARTTLS命令,通知服务器准备加密:

STARTTLS

服务器返回响应消息,通知客户端可以开始加密:

220 Ready to start TLS

接下来就是在TCP连接上建立SSL/TLS加密连接,后续通信以后都基于SSL/TLS连接。

请注意,QQMail服务器目前似乎只支持SSLv2、SSLv3加密,不支持TLSv1.0,TLSv1.1和TLSv1.2加密,其他的SMTP服务器(例如outlook、gmail等)则支持全部加密方式。

2.4 第二次握手

加密连接建立后,再次发送EHLO命令进行握手:

EHLO 192.168.1.101

同样,服务器返回相应能力进行响应。此时,通常不再包含STARTTLS指令了:

250-smtp.qq.com
250-PIPELINING
250-SIZE 73400320
250-AUTH LOGIN PLAIN
250-AUTH=LOGIN
250-MAILCOMPRESS
250 8BITMIME

2.5 账户鉴权

鉴权有多种方式,其中PLAIN方式是要求所有SMTP服务器都必须具备的。在未加密的情况下,采用PLAIN方式无疑会泄露账户密码信息。而在已经加密的SSL/TLS连接里,则不存在这个问题,因此我们采用了最简单的PLAIN方式传递账户密码信息进行鉴权。

PLAIN方式很简单,采用”\0user name\0password”进行编码,然后base64转化成文本格式即可。

客户端发送以下鉴权命令:

AUTH PLAIN my_demo_base64_string_here

服务器鉴权成功,返回响应消息:

235 Authentication successful

2.6 提交邮件地址信息

鉴权成功后,客户端可以开始告诉服务器邮件地址信息,例如发送者、接收者等。此类消息不一一累述,请直接参考以下消息流:

MAIL From:<Iloveu@foxmail.com>
250 Ok
RCPT To:<Iloveu2@msn.com>
250 Ok

2.7 准备递交邮件内容

客户端发送DATA命令,告诉服务器准备提交内容:

DATA

服务器返回响应消息,通知客户端可以开始发送数据:

354 End data with <CR><LF>.<CR><LF>

由于后续数据都采用文本编码,因此标准规定用“\r\n.\r\n”标识数据发送结束,也就是SMTP服务器在上述响应消息中指示的“<CR><LF>.<CR><LF>”。

2.8 发送邮件内容

邮件采用文本格式,附件采用base64编码,可以分多次发送,以下为一个简单示例:

From: <Ilovu@foxmail.com>
To: <Iloveu2@msn.com>
Subject: You have a new voice mail: From 301 Day 2015-12-14 Time 10.29.7
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="==491D4D07305F440E009A0125=="

--==491D4D07305F440E009A0125==
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="From 301 Day 2015-12-14 Time 10.29.7.wav"

UklGRpKaAABXQVZFZm10IBIAAAAGAAEAQB8AAEAfA......
--==491D4D07305F440E009A0125==--
.

对于有附件的情况,应在Content-Type中指明boundary参数,例如本例中的“==491D4D07305F440E009A0125==”。而附件内容起始处用“–boundary参数”标识,附件结尾用“–boundary参数–”标识。

发送完数据后,服务器响应消息:

250 Ok: queued as

2.9 结束

发送完邮件后,客户端友好地向服务器告别:

QUIT

服务器也友好地告别:

221 Bye

最后关闭TCP连接,完成整个流程。