首页 技术 正文
技术 2022年11月17日
0 收藏 493 点赞 2,982 浏览 11442 个字

传统电话是电磁波的通信,当电话技术发展到IP技术时代,SIP协议成为了电话通信标准协议,不仅可以通电话、还可以收发信息、视频、开会、放PPT。事实上,今天的通信业已全面采用SIP协议作为通信标准,无论是固定电话、还是移动电话,其后台都是以SIP协议完成通话、交换的。 很多软件也采用SIP协议进行通信,如:Windows Messenger。

学习多天,记录如下:

一、从打电话的过程,理解SIP协议

(一) 分析一下打电话的过程

两个电话之间的一次通话称为一个会话(Session),

首先,通话双方必须有一个电话号码,  通话步骤如下:

1, 电话A拨打电话B的号码, 邀请B通话 (Invite)

2, 电话B振铃(Ring), 同时电话A可以听到电话B在振铃

3, 电话B提机表示确认应答, 双方通话开始

4, 双方通话

5, 通话过程中,如有任何一方挂电话,则通话结束。

上述会话过程 图示如下:

深入浅出SIP协议

图中箭头表示传递信号的方向。 图中假设B先挂机

在传统电话网中,上述每个信号都是一个电磁波信号.

(二)SIP协议,英文为 Session Initiation Protocol,  中文翻译为会话发起协议。顾名思义,就是在网络上发起会话。

协议(Protocol)是计算机与计算机之间的语言。SIP协议的目的就是在IP网络中实现电话功能。

在IP网络中,通话两端的不是电话机,而是运行在计算机上的软件电话(软电话)。

同传统电话,用SIP协议打一个电话,过程是一样的。两个软电话之间,也有电话号码,也需传递信号。

这时的电话号码是SIP帐号。这时的信号不是一个电磁波信号,而是一个IP数据包(称为SIP消息)。

1, 首先,通话双方都要有一个SIP帐号(也称为URI,  是网络上的电话号码),

不同于全数字的传统电话号码, SIP帐号采用 URI 表示方法, 例如:

sip:peter@company.com:5060

其中:

(1)  sip: 表示采用sip协议

(2) peter是用户名, 也称为帐号.  用字母和数字均可。

(3) company.com 是帐号所属的服务器域名( 也可以用IP地址,例如: sip:peter@192.168.1.100)

(4) 最后的5060是端口号。 SIP协议默认端口为5060,  默认采用UDP传输

:5060的意思是,客户端在名为 company.com的服务器的5060端口号上等待对方连接

如果端口号是 5060,也可以省略不写。

则,上述SIP帐号写为:   sip: peter@company.com

除了sip:这几个字母, SIP帐号就像一个邮件帐号

没错,SIP协议设计者的意图就是让SIP帐号与邮件帐号一致,方便与邮箱服务整合。

对用户来说方便,你要打电话给我,我的电话号就是邮箱号。

2,  SIP 消息

上面讲过,一个通话过程,两端要传递多种信号。在SIP协议中,这些信号是一种约定格式的IP数据包,称为SIP消息。

SIP消息有好几种,让我们看一个通话过程理解一下

深入浅出SIP协议

1, 软电话A 向 B 发送一个 SIP消息  INVITE, 邀请B通话

2, 软电话B振铃,向A 回复一个SIP消息 RING,  通知 A 正在振铃中,请A等待

3, 软电话B提机,向A发一个SIP消息 OK,  通知 A 可以通话了

4, 软电话A 向 B 回复一个回应消息 ACK,正式启动通话

5,接下来,双方通话

6,软电话B挂机,向 A 发一个SIP消息 BYE,   通知 A 通话结束

7, 软电话A 向 B 回复一个消息 OK, 通话结束

可以看到,这个过程与人打电话的过程是一模一样的。只不过是采用IP数据包(SIP消息)的形式传递信号而已。

通话过程中有多种SIP消息,每一种消息都是一个IP数据包。

这就是SIP协议,它约定了会话的发起过程 、结束过程。

二、实操查看 SIP协议通话过程

为了深入SIP协议通话过程,我改编了一个Java软电话程序, 名为sip_test (可在这里下载),这个程序用来显示SIP消息的实际过程。 打开sip_test.exe运行程序,界面如下图:

我的电脑IP是192.168.31.131,  软件自动占用端口54320.  生成了一个SIP帐号:sip:some@192.168.31.131:54320

深入浅出SIP协议

在本机上运行两次sip_test, 则形成两个软电话。这两个软电话可以相互通话了。

把两个软电话窗口左右排列,把左边软件电话的 SIP帐号(My URI那个文本框)复制到右边软电话的 Callee URI(Callee 就是被叫的意思),在右边窗口中点击按键Call开始通话(则右边的软电话是主叫,左边是被叫)。下图红字显示了操作步骤:

深入浅出SIP协议

呼叫开始后,每个软电话均将显示一个小窗口,如下图,再点击左边软电话小窗口中的按键Pickup(提机)接听电话。下图红字显示了操作步骤

深入浅出SIP协议

则进入通话, 如下图。    然后,点击左边软电话小窗口中的按键Hangup(挂机)结束通话。下图红字显示了操作步骤

深入浅出SIP协议

按下按键Hangup(挂机)后,左边的小窗口将消失。  如下图。 然后,再按下右边小窗口的 Close 按键,结束。

深入浅出SIP协议

上述操作过程,完整地操作了一次通话过程:右边的软电话作为主叫,发起呼叫,左边的软电话接听。进入通话,然后左边的软电话先挂机,右边的软电话再Close结束。

软电话的下方文本框中,记录了整个通话过程中发生的SIP消息(如上图)。把右边软电话(主叫) 的记录内容取出来分析一下, 如下:

==================================
2015-02-10 23:53:09,692 SENT to 192.168.31.131/50027
INVITE sip:some@192.168.31.131:50027 SIP/2.0  … … 
==================================
2015-02-10 23:53:09,719 RECEIVED from 192.168.31.131/50027
SIP/2.0 180 Ringing  … … 
==================================
2015-02-11 00:00:14,040 RECEIVED from 192.168.31.131/50027
SIP/2.0 200 OK  … … 
==================================
2015-02-11 00:00:14,226 SENT to 192.168.31.131/50027
ACK sip:192.168.31.131:50027;transport=UDP SIP/2.0  … … 
==================================
2015-02-11 00:04:34,727 RECEIVED from 192.168.31.131/50027
BYE sip:null@192.168.31.131:51971;transport=UDP SIP/2.0  … … 
==================================
2015-02-11 00:04:35,233 SENT to 192.168.31.131/50027
SIP/2.0 200 OK  … …

每一段是一个SIP消息,第一行显示消息发生的时间,同时注明为SENT的是向外发送的消息,RECEIVED为收到的消息。 后面的行显示SIP消息的简要内容

右边软电话作为主叫,几个SIP消息依次是:

1, 发送INVITE

2, 收到Ringing

3, 收到OK

4,  发送 ACK

5,  收到BYE (因为对方先挂机)

6,发送OK

同理,再看左边软电话(被叫)的SIP消息,依次是

1, 收到 INVITE

2,发送Ringing

3,  发送OK (当按下Pickup时)

4, 收到 ACK

5, 发送 BYE (当按下Hangup时)

6, 收到 OK

主叫和被叫的SIP消息画成相对应的下图:(软电话A为主叫,B为被叫)

深入浅出SIP协议

这个实验完整地阐述了SIP协议发起会话、结束会话的典型过程。

话说到这里,大家可能有几个问题:

1, 为什么SIP消息中没有传送语音数据的消息?

答: SIP协议规定了会话的发起过程,但没有规定会话的内容及格式。会话内容可以是文本、语音、视频等。因此,SIP协议要结合其它协议,如:用SDP协议描述要传递的内容格式,用RTP,RTSP流媒体协议传输媒体,才能完成整个通信过程。 SIP协议这样做为了简化协议,留下扩展的灵活性。

对于语音, 处理过程大体是这样:首先把语音录下来成为一组数据,把语音数据进行编码,再发送到对方。对方再解码。

2,SIP消息数据包会不会被偷听?

答:如同 HTTP协议可以叠加SSL保障传输安全。 SIP协议可以叠加TSL安全传输协议。

3,上面过程为什么只是点对点的(P2P)

答:SIP协议规定的是点对点的协议(P2P)。 通话内容的过程可以不需要服务器参与。

实际运用中,大多数情况都有一个代理服务器(Proxy),每个软电话与服务器进行SIP通信即可。

这个服务器就是电话交换机,所有的消息和话音都可以由这个服务器进行转发。

深入浅出SIP协议

三、深入了解SIP消息的内容和结构

上一章展示了SIP消息的种类,本章将深入分析SIP消息的内容和数据格式

首先,在软电话中勾选 Show SIP Message Detail (如下图), 把上一章的操作过程重做一遍。

深入浅出SIP协议

则可以得到,每一个SIP消息的全部数据内容。我们逐一分析一下:

1、INVITE 消息

下面是一个完整的INVITE消息的数据内容

  1. INVITE sip:some@192.168.31.131:50027 SIP/2.0
  2. Via: SIP/2.0/UDP 192.168.31.131:51971;rport;branch=z9hG4bKiYblddPPX
  3. Max-Forwards: 70
  4. To: <sip:some@192.168.31.131:50027>
  5. From: <sip:null@null>;tag=Prf3c3Xc
  6. Call-ID: cenXTa4i-1423587756904@appletekiAir
  7. CSeq: 1 INVITE
  8. Content-Length: 215
  9. Content-Type: application/sdp
  10. Contact: <sip:null@192.168.31.131:51971;transport=UDP>
  11. v=0
  12. o=user1 685988692 621323255 IN IP4 192.168.31.131
  13. s=-
  14. c=IN IP4 192.168.31.131
  15. t=0 0
  16. m=audio 49432 RTP/AVP 0 8 101
  17. a=rtpmap:0 PCMU/8000
  18. a=rtpmap:8 PCMA/8000
  19. a=rtpmap:101 telephone-event/8000
  20. a=sendrecv

SIP协议是由国际互联网协会(IETF)制定的,这个协会也制订了HTTP协议。所以,这两个协议有很多地方是相似的。

与HTTP协议相同,SIP协议也采用Client/Server模式,Client发请求,Server响应,请求和响应消息均采用纯文本方式。

INVITE 是主叫做为Client, 被叫做为Server, 发起的一个请求 (类似HTTP请求,例如POST)

请求消息分成几个部分,一是起始行(start line),  二是消息头部(message head),内含一个或多个头字段(head field), 三是一个空行(empty line),表示头部的结束。四是一个消息体(message body).  格式如下:

generic-message = start-line

*message-header

CRLF

[message-body]

(1)起始行(start-line):

INVITE sip:some@192.168.31.131:50027 SIP/2.0

请求消息的起始行包括三个参数,格式:  Request start-line = Method URI SIP_VERSION

Method是请求方法,本例是INVITE,   SIP协议规定的Method有六种: INVITE, ACK, CANCEL用于创建对话,BYE用于结束对话, REGISTER用于登记,OPTIONS用于查询服务器能力

URI表示所请求的用户或服务器,  也支持 “tel” URI, 本例是sip:some@192.168.31.131:50027,

SIP_VERSION是 SIP版本号,本例是 SIP/2.0

(2)消息头部(header)

头部包含一个或多个头字段(head field), 每个字段一行。

一个字段行 = name : value ;  value;

一个请求消息头部至少要包含六个字段:Via, To, From, CSeq, Caller-ID, Max-Forwards

Via: SIP/2.0/UDP 192.168.31.131:51971;rport;branch=z9hG4bKiYblddPPX

Max-Forwards: 70
To: <sip:some@192.168.31.131:50027>
From: <sip:null@null>;tag=Prf3c3Xc
Call-ID: cenXTa4i-1423587756904@appletekiAir
CSeq: 1 INVITE
Content-Length: 215
Content-Type: application/sdp
Contact: <sip:null@192.168.31.131:51971;transport=UDP>

以下逐个解释每个字段的含义

名词解释:

User Agent Client(UAC):用户代理客户端。用户代理客户端是一个逻辑的概念,他创建一个新请求,并且用客户事务状态机发送这个请求。UAC角色只在事务中存在。换句话说,UAC就是一小段代码初始化一个请求,并且在事务中遵循UAC的规则。如果它接下来收到一个请求,那么在那个事务中,它就是作为UAS来处理请求。
User Agent Server(UAS): 用户代理服务器.UAS是一个逻辑的实体,对SIP请求做响应的。应答接受、拒绝、或者转发对应的请求。UAS角色在事务中存在。换句话说,是响应请求的一小段软件,在事务中作为UAS存在。如果他发出请求,那么他就在事务中作为UAC存在。

I.  Via字段

Via: SIP/2.0/UDP 192.168.31.131:51971;rport;branch=z9hG4bKiYblddPPX

Via头字段保存所经过SIP网元(客户端或Proxy)的主机名或网络地址(可能还有端口号),消息中的所有Via头字段对请求消息而言,从下至上依次表示到当前所在SIP网元为止,请求消息所经过的路径;对响应消息而言,从上至下依次表示从当前网元开始,响应所应遵循的路径。

Via字段包含SIP协议版本以及消息传输所用的传输协议, 此例为:  SIP/2.0/UDP

branch参数:  在SIP网元(UAC或Proxy)发出或转发请求消息时,在其插入的Via字段中必须包含branch参数,该参数用于标识此请求消息所创建的事务。branch 参数可以用做loop detection,这时参数必须被分成两部分:第一部分符合一般的原则(对于RFC3261,z9hG4bK),第二部分(此例为iYblddPPX)被用来实现loop detection以用来区分loop和spiral。loop和spiral均指Proxy收到一个请求后转发,然后此转发的请求又重新到达该Proxy,区别是loop中请求的Request-URI以及其他影响Proxy处理的头字段均不变,而Spiral请求中这些部分必需有某个发生改变,spiral发生的典型情况是Request-URI发生改变。Proxy在插入Via字段前,其branch 参数的loop.  detection部分依据以下元素编码:To Tag,From Tag,Call-ID字段,Request-URI,Topmost Via字段,Cseq的序号部分(即与request method无关),以及proxy-require字段,proxy authorization字段。注意:request method不能用于计算branch参数,比如CANCEL以及非2XX response的ACK与其所cancel的request或对应的INVITE属于同一个事务,即其branch参数相同。见RFC3261 P22 P25 P39 P95 P105

II. Max-Forwards 字段

Max-Forwards: 70

Max-Forwards 字段表示request到达UAS的跳数的限制。是一个整数,经过每一跳时减去一。如果Max-Forwards已经是零,可是request还没有到达目的地,则就会产生一个483(too many hops)响应

III. To字段

To: <sip:some@192.168.31.131:50027>

To字段表示消息的接收者

To 字段可以有一个tag参数,to tag代表dialog的对等参与者(peer)。在UAC发出一个初始Dialog的请求(如INVITE)时,即发出out-of-dialog请求时,由于dialog还没有建立,不含to tag参数。当UAS收到INVITE请求时,在其发出的2xx或101-199响应中设置to tag参数,与UAC设置的From Tag参数以及Call-ID(呼叫唯一标识)一起作为一个Dialog ID(对话唯一标识,包含To tag,From Tag,Call-ID)的一个部分。RFC3261规定只有INVITE请求与2xx或101-199响应可以建立Dialog(由101-199响应创建的Dialog称为early dialog)。见RFC3261 P70

IV. From字段

From: <sip:null@null>;tag=Prf3c3Xc

From字段表示消息的发送者

From字段必须包含tag参数,在UAC发出一个out-of-dialog请求(对话建立请求)时,必须设置一个唯一的tag参数,作为Dialog ID的一个部分。 
见RFC3261 P37,P159,P172

V. Call-ID字段

Call-ID: cenXTa4i-1423587756904@appletekiAir

是一个邀请(Invitation)或来自同一个UAC用户的所有登记请求
(Registeration,包括更新登记,取消登记)以及由此产生的一组响应的唯一标识。一个邀请可以建立多个Dialog(当被叫用户有多个联系方式时),这成为Forking,因而Call-ID只是一次呼叫邀请的唯一标识,Call-ID与UAC在发出请求中设置的From Tag字段以及UAS在其相映中设置的To Tag字段三者一起作为一个Dialog-ID。 
在一个Dialog中,所有的requests和responses的Call-ID必须一致 同一UA的每一个register 的Call-ID必须一致。 见RFC3261 P37,P159,P166

VI. CSeq 字段

CSeq: 1 INVITE

用于在同一个Dialog中标识及排序事务(transaction)以及区分新的请求 与请求的重发。

CSeq包括顺序号和方法(method),方法必须和它所对应的request相匹配。对于out-of-dialog的非register request,取值任意。 
对于dialog内的每一个新的request(如BYE,re-INVITE,OPTION),Cseq的序号加1。但是对于CANCEL,ACK除外。对于ACK而言,Cseq的序号必须与其所对应的request相同。对于CANCEL而言,Cseq的序号也必须与其cancel掉的request相同。 
注意:在同一个对话中的UAC和UAS分别维护自己的CSeq序号,他们发出请求的CSeq序号是不相关的。见RFC3261 P218 见RFC3261 P38,P159,P170,P218

VII. Contact 字段

Contact: <sip:null@192.168.31.131:51971;transport=UDP>

对于非Register事务,Contact header field 主要提供了UAC或UAS的 直接联系SIP URI,UAC在发出的对话建立(out-of-dialog)INVITE请求的Contact字段中提供自己的直接联系SIP URI,在UAS收到该请求后在其发出响应的Contact字段中提供自己的直接联系SIP URI,这样在建立对话后,UA间可以通过对方的直接联系SIP URI绕过Proxy直接发送请求。  对于Register事务,表示地址绑定中的contact address(vs. address-of-record)

VIII. Content-Type字段

Content-Type: application/sdp

主要表示发给接收器的消息体的媒体类型。如果消息体不是空的,则Content-type header field一定要存在。如果Content-type header field存在,而消息体是空的,表明该

类型的媒体流长度是0。

VIIII. Content-Length字段

Content-Length: 215

表示消息体的长度。是十进制数。

(3)消息体(message body)

编码方式主要是由头部确定,现在为一般为SDP。

v=0            //版本号为0
o=user1 685988692 621323255 IN IP4 192.168.31.131

//建立者用户名+会话ID+版本+网络类型+地址类型+地址 
s=-            //会话名
c=IN IP4 192.168.31.131  //连接信息:网络类型+地址类型+地址
t=0 0         //会话活动时间 起始时间+终止时间
m=audio 49432 RTP/AVP 0 8 101

//媒体描述:媒体+端口+传送+格式列表

音频 + 端口49432 + 传输协议RTP + 格式AVP,有效负荷0(u率PCM编码) 
a=rtpmap:0 PCMU/8000

//0或多个会话属性: 属性 + 有效负荷+ 编码名称 + 抽样频率。

//                                   rtpmap +   0型  +  PCMU  +  8KHz 
a=rtpmap:8 PCMA/8000

//a 可以有多个, 见SDP协议
a=rtpmap:101 telephone-event/8000
a=sendrecv

2、Ringing 消息

Ringing消息是被叫对主叫INVITE请求消息的响应消息, 表示被叫正在振铃

SIP/2.0 180 Ringing
From: <sip:null@null>;tag=Prf3c3Xc
Call-ID: cenXTa4i-1423587756904@appletekiAir
CSeq: 1 INVITE
Via: SIP/2.0/UDP 192.168.31.131:51971;rport=51971;branch=z9hG4bKiYblddPPX
To: <sip:some@192.168.31.131:50027>;tag=AM1g60xRvq

Contact: <sip:192.168.31.131:50027;transport=UDP>

首行(start-line)格式是: SIP协议号 代码 文本描述

Ring消息的代码是180

消息头部其它字段均复制INVITE消息

3、OK 消息

OK消息是被叫对主叫INVITE请求消息的最终响应消息, 表示被叫已提机接听

SIP/2.0 200 OK
From: <sip:null@null>;tag=Prf3c3Xc
Call-ID: cenXTa4i-1423587756904@appletekiAir
CSeq: 1 INVITE
Via: SIP/2.0/UDP 192.168.31.131:51971;rport=51971;branch=z9hG4bKiYblddPPX
To: <sip:some@192.168.31.131:50027>;tag=AM1g60xRvq
Content-Length: 214
Content-Type: application/sdp

Contact: <sip:192.168.31.131:50027;transport=UDP>

v=0
o=user1 77115499 915054303 IN IP4 192.168.31.131
s=-
c=IN IP4 192.168.31.131
t=0 0
m=audio 49434 RTP/AVP 0 8 101
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000

a=sendrecv

首行(start-line)格式是: SIP协议号 代码 文本描述

Ring消息的代码是180

消息头部其它字段均复制INVITE消息

消息体是被叫的SDP描述

4、ACK 消息

主叫在接收到OK响应消息后,向被叫发送ACK请求消息的响应消息, 正式启动通话

ACK消息没有消息体

ACK sip:192.168.31.131:50027;transport=UDP SIP/2.0
Via: SIP/2.0/UDP 192.168.31.131:51971;rport;branch=z9hG4bKEfwYu4LbB
To: <sip:some@192.168.31.131:50027>;tag=AM1g60xRvq
From: <sip:null@null>;tag=Prf3c3Xc
Call-ID: cenXTa4i-1423587756904@appletekiAir
CSeq: 3 ACK

Max-Forwards: 70

5、BYE 消息

通话过程中,主动挂机的一方向另一方发送BYE请示消息,表示请求结束通话

另一方接到BYE消息后,应回复OK消息

BYE sip:null@192.168.31.131:51971;transport=UDP SIP/2.0
Via: SIP/2.0/UDP 192.168.31.131:50027;rport;branch=z9hG4bKvtPAT0lfO
To: <sip:null@null>;tag=Prf3c3Xc
From: <sip:some@192.168.31.131:50027>;tag=AM1g60xRvq
Call-ID: cenXTa4i-1423587756904@appletekiAir
CSeq: 711793880 BYE
Max-Forwards: 70

花了很长的篇幅,说了一下SIP消息的结构。有些难懂。

了解所有的数据结构,可以最终实现一个自己的SIP。看来,还有一段长路要走。

四、构建自己的VOIP电话系统

SIP协议是用于在IP网络中实现通话的,也就是VOIP(Voice Over IP)。

有很多开源软件实现了SIP协议,可以很容易构建一套属于自己的VOIP系统。这样,就可以在IP网络中打电话、发短信、视频了。

一个VOIP系统中最少有两个部件:  一个SIP服务器,   多个SIP客户端

SIP客户端分为SIP软件和SIP话机两种。

SIP软件可以安装在手机或PC上,用它来打电话。

SIP软件手机版的推荐两个:   CSipSimple,   Linphone ,   在各个手机应用市场中均可下载。  PC版的我还觉得不好用,毕竞我们很少用PC打电话.

SIP话机外形与普通电话机一样,不同的是它不是连接在电话线上,而是直接连在网络上,像电话一样使用。

SIP服务器是相当于电话交换机(PBX), 用于连接各个客户端,可以架在公网上实现随处打电话,  也可以架在局域网上实现办公室内打电话。

SIP服务器多用Linux操作系统,安装SIP服务器软件。 软件我推荐用 Asterisk, 这是最老牌最成熟的SIP服务器,已经被集成在Debian, Ubuntu、CentOS、OpenWrt等多个Linux发行版中,安装很容易。  很多IP-PBX也采用Asterisk作为内核。

关于如何构建自己的电话系统, 通信系列的下一篇将详细介绍。

转自:http://blog.csdn.net/c80486/article/details/43391961

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,020
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,513
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,359
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,142
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,772
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,850