最近公司做一个新项目,后台是webService,要做iOS版本的客户端,由于我以前不怎么了解这方面,所以云里雾里得摸索了两天,因为后台人员也不清楚我具体要传什么给他,所以。。。。我就两边跑。。。。最终我确定问题还是在于后端,今天上午完美解决,对webService和WSDL以及CXF有了个大概的了解。
我就不做过多的概念介绍了,我很懒不愿复制粘贴,只要说一些细节让大家少走弯路就可以。
在继续往下之前你需要先去了解一些概念:SOAP、WSDL、CXF,和他们之间的关系。这里我觉得这个帖子比较好,推荐一下,Web Service笔记(三):wsdl 与 soap协议详解 , 对XML或者HTML稍微有点了解看了这篇文章之后对WSDL基本都能大体了解了,这里也感谢一下作者。
假定现在你对它们有个大体的了解,SOAP请求就是你发一段XML给后台,然后后台返回数据给你,它是通用的,参数后台会在XML中提取,所以我们在这个过程中其实就是在于传的XML的内容,本文也会讲到中间遇到的一些细节。
首先不要把它想的很复杂,弄通了封装一下,其实这种方式传的东西个人感觉和普通的GET/POST差不多,甚或更方便。WSDL文档由于我现在不在公司连不上服务器所以就不介绍
首先XML内容,你的SOAP协议版本要和后台一致,不然后台报错会说版本不一致之类的,这里列出,请自行对比。
SOAP 1.1
以下是 SOAP 1.1 请求和响应示例。所显示的占位符需替换为实际值。
POST /WebServices/MobileCodeWS.asmx HTTP/1.1
Host: webservice.webxml.com.cn
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://WebXml.com.cn/getMobileCodeInfo"
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAP 1.2
以下是 SOAP 1.2 请求和响应示例。所显示的占位符需替换为实际值。
POST /WebServices/MobileCodeWS.asmx HTTP/1.1
Host: webservice.webxml.com.cn
Content-Type: application/soap+xml; charset=utf-8
Content-Length: length
HTTP/1.1 200 OK
Content-Type: application/soap+xml; charset=utf-8
Content-Length: length
直接说注意点,拿SOAP 1.1 请求做例子,要传的XML文档为
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<getMobileCodeInfo xmlns="http://WebXml.com.cn/">
<mobileCode>string</mobileCode>
<userID>string</userID>
</getMobileCodeInfo>
</soap:Body>
</soap:Envelope>
<soap: Body>标签以外的不用改,head一般也不用传,要调用的方法和参数都包在body里面,比如这个例子中,getMobileCodeInfo是WSDL文档发布的你要调用的方法名,其后接的xmlns是你wsdl文档中对应的targetNameSpace,这个和你JAVA中的package名对应,比如你后台JAVA代码中该方法中import的package为 com.xxxx.oooo 那么这里的xmlns就是 ooo.xxxx.com ,mobileCode和userID是这个方法中接收的参数名,那么也就是说该方法在其JAVA后台大概是这个样子
public getMobileCodeInfo (@WebParam(name="mobileCode" , name="userID") String mobileCode, String userID),对应的WSDL中该operation的input下也应该有两个name就是mobileCode和userID,注意@WebParam(name="mobileCode" , name="userID") 不能少,这个注解让后台认识参数名,少了后台会一直报unexpected element ,我们后台一开始就少了这个,搞了很久。这种情况是参数分开传的情况,有的后台的参数直接一个request,但是里面包含有几个键值对也其实就是相当于几个参数,刚接触的新手在这里很容易绕弯,比如我后台有这个个方法 public login (WebParam(name="req" ) String req) ,很明显这里只接一个参数,而且叫做req,但是这个请求就是需要账户名和密码,所以在对应的xml中文档应这样写body中内容
,最后请求地址就是你的wsdl文档中的Endpoint address地址,请求体就是该XML,发个POST请求就完事啦,这里又有事了,本人是强迫症,因为一开始就用的AFN请求,咋请求咋报错,于是跟着网上找的DEMO换NSURLConnection,一不小心成功了一次,但是这个iOS已经废弃了不喜欢,于是用NSURLSession,恩也成功了,但是还是感觉好麻烦,发个NSURLSession请求soap的代码
经过和后台七改八改都无果,我干脆自己了解这些文档规范,在上午确定我操作没错之后,直接说后台那边有问题,然后后台改,重启服务器我请求没有任何进展,最后竟然是什么问题你们知道么。。前一天其实我已经传对了XML,只是后台改动的代码放错了地方,只放到了正式服没有放测试服,我也是,,,醉了,,不过也好,要是那么顺畅我就不会去过多了解这方面了。
好了,要传什么,注意点都讲了,现在到了客户端的问题,用官方SDK请求是不是感觉很麻烦?是的,对于用惯了AFN或者自己封装的网络请求工具类的人来说如果每次都要写这多么代码发一次请求太痛苦了,于是我想可不可以用AFN请求SOAP,一开始想用manager发请求,直接把XML当params发POST肯定是直接挂了,于是想要设置HTTPBody要不用AFHTTPRequestOperation?没错这样确实可以,代码如下:
这一看感觉和NSURLSession没多大差别,还是想用manager,关键问题就在于设置request的HTTBbody为XML,但是AFHTTPSessionManager已经把request封装了,默认用的params,怎么改?于是想改动或者添加AFN内部方法,但是总感觉这样不好,万一更新库了又要搞一遍。于是想能否拦截这个request,或者通过manager.requestSerializer设置HTTPBody,敲set浏览一下没有HTTPBody字眼的,用KVC也不行,那样还是相当于把XML当params传了,伤心绝望之时看到这个方法
[manager.requestSerializer setQueryStringSerializationWithBlock:^NSString *(NSURLRequest *request, NSDictionary *parameters, NSError *__autoreleasing *error) {
//
}]
一看里面有request 有 params 高兴了,说不定在这里能拦截,于是直接写
[manager.requestSerializer setQueryStringSerializationWithBlock:^NSString *(NSURLRequest *request, NSDictionary *parameters, NSError *__autoreleasing *error) {
return soapStr;
}],请求成功,高兴了。
接下来另一个问题了,简单封装一下,我收到的是二进制,所以把AFN封装成了个工具类,供大家参考,欢迎提出改进
/**
* 请求SOAP,返回NSData
*
* @param url 请求地址
* @param soapBody soap的XML中方法和参数段
* @param success 成功block
* @param failure 失败block
*/
我就不做过多的概念介绍了,我很懒不愿复制粘贴,只要说一些细节让大家少走弯路就可以。
在继续往下之前你需要先去了解一些概念:SOAP、WSDL、CXF,和他们之间的关系。这里我觉得这个帖子比较好,推荐一下,Web Service笔记(三):wsdl 与 soap协议详解 , 对XML或者HTML稍微有点了解看了这篇文章之后对WSDL基本都能大体了解了,这里也感谢一下作者。
假定现在你对它们有个大体的了解,SOAP请求就是你发一段XML给后台,然后后台返回数据给你,它是通用的,参数后台会在XML中提取,所以我们在这个过程中其实就是在于传的XML的内容,本文也会讲到中间遇到的一些细节。
首先不要把它想的很复杂,弄通了封装一下,其实这种方式传的东西个人感觉和普通的GET/POST差不多,甚或更方便。WSDL文档由于我现在不在公司连不上服务器所以就不介绍
首先XML内容,你的SOAP协议版本要和后台一致,不然后台报错会说版本不一致之类的,这里列出,请自行对比。
SOAP 1.1
以下是 SOAP 1.1 请求和响应示例。所显示的占位符需替换为实际值。
POST /WebServices/MobileCodeWS.asmx HTTP/1.1
Host: webservice.webxml.com.cn
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://WebXml.com.cn/getMobileCodeInfo"
1
2
3
4
5
6
7
8
9
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
soap:Envelope
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd
=
"http://www.w3.org/2001/XMLSchema"
xmlns:soap
=
"http://schemas.xmlsoap.org/soap/envelope/"
>
<
soap:Body
>
<
getMobileCodeInfo
xmlns
=
"http://WebXml.com.cn/"
>
<
mobileCode
>string</
mobileCode
>
<
userID
>string</
userID
>
</
getMobileCodeInfo
>
</
soap:Body
>
</
soap:Envelope
>
|
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length
1
2
3
4
5
6
7
8
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
soap:Envelope
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd
=
"http://www.w3.org/2001/XMLSchema"
xmlns:soap
=
"http://schemas.xmlsoap.org/soap/envelope/"
>
<
soap:Body
>
<
getMobileCodeInfoResponse
xmlns
=
"http://WebXml.com.cn/"
>
<
getMobileCodeInfoResult
>string</
getMobileCodeInfoResult
>
</
getMobileCodeInfoResponse
>
</
soap:Body
>
</
soap:Envelope
>
|
SOAP 1.2
以下是 SOAP 1.2 请求和响应示例。所显示的占位符需替换为实际值。
POST /WebServices/MobileCodeWS.asmx HTTP/1.1
Host: webservice.webxml.com.cn
Content-Type: application/soap+xml; charset=utf-8
Content-Length: length
1
2
3
4
5
6
7
8
9
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
soap12:Envelope
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd
=
"http://www.w3.org/2001/XMLSchema"
xmlns:soap12
=
"http://www.w3.org/2003/05/soap-envelope"
>
<
soap12:Body
>
<
getMobileCodeInfo
xmlns
=
"http://WebXml.com.cn/"
>
<
mobileCode
>string</
mobileCode
>
<
userID
>string</
userID
>
</
getMobileCodeInfo
>
</
soap12:Body
>
</
soap12:Envelope
>
|
HTTP/1.1 200 OK
Content-Type: application/soap+xml; charset=utf-8
Content-Length: length
1
2
3
4
5
6
7
8
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
soap12:Envelope
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd
=
"http://www.w3.org/2001/XMLSchema"
xmlns:soap12
=
"http://www.w3.org/2003/05/soap-envelope"
>
<
soap12:Body
>
<
getMobileCodeInfoResponse
xmlns
=
"http://WebXml.com.cn/"
>
<
getMobileCodeInfoResult
>string</
getMobileCodeInfoResult
>
</
getMobileCodeInfoResponse
>
</
soap12:Body
>
</
soap12:Envelope
>
|
直接说注意点,拿SOAP 1.1 请求做例子,要传的XML文档为
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<getMobileCodeInfo xmlns="http://WebXml.com.cn/">
<mobileCode>string</mobileCode>
<userID>string</userID>
</getMobileCodeInfo>
</soap:Body>
</soap:Envelope>
<soap: Body>标签以外的不用改,head一般也不用传,要调用的方法和参数都包在body里面,比如这个例子中,getMobileCodeInfo是WSDL文档发布的你要调用的方法名,其后接的xmlns是你wsdl文档中对应的targetNameSpace,这个和你JAVA中的package名对应,比如你后台JAVA代码中该方法中import的package为 com.xxxx.oooo 那么这里的xmlns就是 ooo.xxxx.com ,mobileCode和userID是这个方法中接收的参数名,那么也就是说该方法在其JAVA后台大概是这个样子
public getMobileCodeInfo (@WebParam(name="mobileCode" , name="userID") String mobileCode, String userID),对应的WSDL中该operation的input下也应该有两个name就是mobileCode和userID,注意@WebParam(name="mobileCode" , name="userID") 不能少,这个注解让后台认识参数名,少了后台会一直报unexpected element ,我们后台一开始就少了这个,搞了很久。这种情况是参数分开传的情况,有的后台的参数直接一个request,但是里面包含有几个键值对也其实就是相当于几个参数,刚接触的新手在这里很容易绕弯,比如我后台有这个个方法 public login (WebParam(name="req" ) String req) ,很明显这里只接一个参数,而且叫做req,但是这个请求就是需要账户名和密码,所以在对应的xml中文档应这样写body中内容
1
2
3
4
5
|
<
soap12:Body
>
<
login
xmlns
=
"http://WebXml.com.cn/"
>
<
req
>{@"userNameKey": zhangsan, @"password": pwdpwd}</
req
>
</
login
>
</
soap12:Body
>
|
,最后请求地址就是你的wsdl文档中的Endpoint address地址,请求体就是该XML,发个POST请求就完事啦,这里又有事了,本人是强迫症,因为一开始就用的AFN请求,咋请求咋报错,于是跟着网上找的DEMO换NSURLConnection,一不小心成功了一次,但是这个iOS已经废弃了不喜欢,于是用NSURLSession,恩也成功了,但是还是感觉好麻烦,发个NSURLSession请求soap的代码
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
|
NSString
*soapStr = [
NSString
stringWithFormat:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\
<soap:Envelope xmlns:xsi=\"http:
//www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\
xmlns:soap=\"http:
//schemas.xmlsoap.org/soap/envelope/\">\
<soap:Header>\
</soap:Header>\
<soap:Body>\
<login xmlns=\"targetNameSpace地址/\">\
<param1>test</param1>\
<param2>test</param2>\
</login>\
</soap:Body>\
</soap:Envelope>"];
NSURL
*url=[
NSURL
URLWithString:@
"你自己wsdl文档中对应的endpoint address"
];
NSMutableURLRequest
*request=[
NSMutableURLRequest
requestWithURL:url];
// 访问方式
[request setHTTPMethod:@
"POST"
];
// 设置请求头(请求头也可以不设置,前两个设不设置都一样,应该默认的,但是SOAPAction我怎么都设置不对,不设置也可以,干脆不设置了)
// [request addValue:@"text/xml; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
// [request addValue:[NSString stringWithFormat:@"%zd", soapStr.length] forHTTPHeaderField:@"Content-Length"];
// [request addValue:@"nameSpace/methodName" forHTTPHeaderField:@"SOAPAction"];
// body内容
[request setHTTPBody:[soapStr dataUsingEncoding:
NSUTF8StringEncoding
]];
NSURLSession
*session = [
NSURLSession
sharedSession];
NSURLSessionDataTask
*task = [session dataTaskWithRequest:request completionHandler:^(
NSData
* _Nullable data,
NSURLResponse
* _Nullable response,
NSError
* _Nullable error) {
NSString
*result = [[
NSString
alloc] initWithData:data encoding:
NSUTF8StringEncoding
];
WJLog(@
"进入成功回调Session-----结果:%@----请求地址:%@"
, result, response.URL);
if
(error) {
WJLog(@
"Session----失败----%@"
, error.localizedDescription);
}
}];
[task resume];
|
经过和后台七改八改都无果,我干脆自己了解这些文档规范,在上午确定我操作没错之后,直接说后台那边有问题,然后后台改,重启服务器我请求没有任何进展,最后竟然是什么问题你们知道么。。前一天其实我已经传对了XML,只是后台改动的代码放错了地方,只放到了正式服没有放测试服,我也是,,,醉了,,不过也好,要是那么顺畅我就不会去过多了解这方面了。
好了,要传什么,注意点都讲了,现在到了客户端的问题,用官方SDK请求是不是感觉很麻烦?是的,对于用惯了AFN或者自己封装的网络请求工具类的人来说如果每次都要写这多么代码发一次请求太痛苦了,于是我想可不可以用AFN请求SOAP,一开始想用manager发请求,直接把XML当params发POST肯定是直接挂了,于是想要设置HTTPBody要不用AFHTTPRequestOperation?没错这样确实可以,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
NSMutableURLRequest
*request=[
NSMutableURLRequest
requestWithURL:url];
[request setHTTPMethod:@
"POST"
];
[request setHTTPBody:[soapStr dataUsingEncoding:
NSUTF8StringEncoding
]];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
// 设置返回数据格式
operation.responseSerializer = [AFHTTPResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation * _Nonnull operation,
id
_Nonnull responseObject) {
NSString
*result = [[
NSString
alloc] initWithData:responseObject encoding:
NSUTF8StringEncoding
];
WJLog(@
"AFN--成功--结果:%@----返回数据%@"
, result, responseObject);
} failure:^(AFHTTPRequestOperation * _Nonnull operation,
NSError
* _Nonnull error) {
WJLog(@
"AFN--失败--%@"
, error.localizedDescription);
}];
[operation start];
|
这一看感觉和NSURLSession没多大差别,还是想用manager,关键问题就在于设置request的HTTBbody为XML,但是AFHTTPSessionManager已经把request封装了,默认用的params,怎么改?于是想改动或者添加AFN内部方法,但是总感觉这样不好,万一更新库了又要搞一遍。于是想能否拦截这个request,或者通过manager.requestSerializer设置HTTPBody,敲set浏览一下没有HTTPBody字眼的,用KVC也不行,那样还是相当于把XML当params传了,伤心绝望之时看到这个方法
[manager.requestSerializer setQueryStringSerializationWithBlock:^NSString *(NSURLRequest *request, NSDictionary *parameters, NSError *__autoreleasing *error) {
//
}]
一看里面有request 有 params 高兴了,说不定在这里能拦截,于是直接写
[manager.requestSerializer setQueryStringSerializationWithBlock:^NSString *(NSURLRequest *request, NSDictionary *parameters, NSError *__autoreleasing *error) {
return soapStr;
}],请求成功,高兴了。
接下来另一个问题了,简单封装一下,我收到的是二进制,所以把AFN封装成了个工具类,供大家参考,欢迎提出改进
/**
* 请求SOAP,返回NSData
*
* @param url 请求地址
* @param soapBody soap的XML中方法和参数段
* @param success 成功block
* @param failure 失败block
*/
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
|
+ (
void
)SOAPData:(
NSString
*)url soapBody:(
NSString
*)soapBody success:(
void
(^)(
id
responseObject))success failure:(
void
(^)(
NSError
*error))failure {
NSString
*soapStr = [
NSString
stringWithFormat:
@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\
<soap:Envelope xmlns:xsi=\"http:
//www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\
xmlns:soap=\"http:
//schemas.xmlsoap.org/soap/envelope/\">\
<soap:Header>\
</soap:Header>\
<soap:Body>%@</soap:Body>\
</soap:Envelope>",soapBody];
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFXMLParserResponseSerializer serializer];
// 设置请求超时时间
manager.requestSerializer.timeoutInterval = 30;
// 返回NSData
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
// 设置请求头,也可以不设置
[manager.requestSerializer setValue:@
"application/soap+xml; charset=utf-8"
forHTTPHeaderField:@
"Content-Type"
];
[manager.requestSerializer setValue:[
NSString
stringWithFormat:@
"%zd"
, soapStr.length] forHTTPHeaderField:@
"Content-Length"
];
// 设置HTTPBody
[manager.requestSerializer setQueryStringSerializationWithBlock:^
NSString
*(
NSURLRequest
*request,
NSDictionary
*parameters,
NSError
*__autoreleasing *error) {
return
soapStr;
}];
[manager POST:url parameters:soapStr success:^(
NSURLSessionDataTask
* _Nonnull task,
id
_Nonnull responseObject) {
// 把返回的二进制数据转为字符串
NSString
*result = [[
NSString
alloc] initWithData:responseObject encoding:
NSUTF8StringEncoding
];
// 利用正则表达式取出<return></return>之间的字符串
NSRegularExpression
*regular = [[
NSRegularExpression
alloc] initWithPattern:@
"(?<=return\\>).*(?=</return)"
options:
NSRegularExpressionCaseInsensitive
error:
nil
];
NSDictionary
*dict = [
NSDictionary
dictionary];
for
(
NSTextCheckingResult
*checkingResult in [regular matchesInString:result options:0 range:
NSMakeRange
(0, result.length)]) {
// 得到字典
dict = [
NSJSONSerialization
JSONObjectWithData:[[result substringWithRange:checkingResult.range] dataUsingEncoding:
NSUTF8StringEncoding
] options:
NSJSONReadingMutableLeaves
error:
nil
];
}
// 请求成功并且结果有值把结果传出去
if
(success && dict) {
success(dict);
}
} failure:^(
NSURLSessionDataTask
* _Nullable task,
NSError
* _Nonnull error) {
if
(failure) {
failure(error);
}
}];
}
|