在上一篇,我们让iOS设备通过AirTunes发现了Android设备链接。
这一篇,我们将完成iOS设备通过AirTunes连接上Android设备。
三、实现iOS设备通过AirTunes连接上Android devices
- 1 使用netty构造一个server,设定基础配置。
final ServerBootstrap airTunesBootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(executorService, executorService));
airTunesBootstrap.setOption("reuseAddress", true); //端口重用
airTunesBootstrap.setOption("child.tcpNoDelay", true);
airTunesBootstrap.setOption("child.keepAlive", true); //保持连接
- 2 给ServerBootstrap自定义PiplineFactory,并且建立5个Handler。
添加到server配置
airTunesBootstrap.setPipelineFactory(new RaopRtsPipelineFactory());
try {channelGroup.add(airTunesBootstrap.bind(new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), getRtspPort())));
}
catch (Exception e) {
LOG.log(Level.SEVERE, "error",e);
try {channelGroup.add(airTunesBootstrap.bind(new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), getAnotherRtspPort())));
}
catch (Exception e1) {
LOG.log(Level.SEVERE, "error",e1);
}
}
给PiplineFactory添加rtsp decode和encode的handler,并添加Airtunes连接需要的另外三个核心handler。
public class RaopRtsPipelineFactory implements ChannelPipelineFactory {
@Override
public ChannelPipeline getPipeline() throws Exception {
final ChannelPipeline pipeline = Channels.pipeline();
//因为是管道 注意保持正确的顺序
pipeline.addLast("decoder", new RtspRequestDecoder());//RtspRequestDecoder
pipeline.addLast("encoder", new RtspResponseEncoder());//RtspResponseEncoder
pipeline.addLast("challengeResponse", new RaopRtspChallengeResponseHandler(NetworkUtils.getInstance().getHardwareAddress()));
pipeline.addLast("header", new RaopRtspHeaderHandler());
pipeline.addLast("options", new RaopRtspOptionsHandler());
return pipeline;
}
}
A. RaopRtspChallengeResponseHandler详解:
- messageReceived:苹果设备向Android设备中发送的Request的Header中包含一个叫"Apple-Challenge"的字段。这个字段需要通过base64的解密获取凭证,该凭证会在回复信息给苹果设备时候使用。*
@Override
public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent evt)
throws Exception
{
final HttpRequest req = (HttpRequest)evt.getMessage();
synchronized(this) {
if (req.containsHeader(HeaderChallenge)) {
/* The challenge is sent without padding! */
final byte[] challenge = Base64.decodeUnpadded(req.getHeader(HeaderChallenge));
/* Verify that we got 16 bytes */
if (challenge.length != 16)
throw new ProtocolException("Invalid Apple-Challenge header, " + challenge.length + " instead of 16 bytes");
/* Remember challenge and local address.
* Both are required to compute the response
*/
m_challenge = challenge;
m_localAddress = ((InetSocketAddress)ctx.getChannel().getLocalAddress()).getAddress();
}
else {
/* Forget last challenge */
m_challenge = null;
m_localAddress = null;
}
}
super.messageReceived(ctx, evt);
}
- writeRequested:在回复给苹果设备时,需要构造"Apple-Response"的key,其值为challengeResponse解密后16位凭证 + 16位的ipv6 address + 6位的网络硬件地址。这个里面的加密所使用的rsa的privtekey,来自于国外大神的破译。
@Override
public void writeRequested(final ChannelHandlerContext ctx, final MessageEvent evt)
throws Exception
{
final HttpResponse resp = (HttpResponse)evt.getMessage();
synchronized(this) {
if (m_challenge != null) {
try {
/* Get appropriate response to challenge and
* add to the response base-64 encoded. XXX
*/
final String sig = Base64.encodePadded(getSignature());
resp.setHeader(HeaderSignature, sig);
}
finally {
/* Forget last challenge */
m_challenge = null;
m_localAddress = null;
}
}
}
super.writeRequested(ctx, evt);
}
B. RaopRtspHeaderHandler详解:
- 对于Rtsp Header来说,每一个都包含一个CSeq的头,在request和response中保持一致。每个response的 RTSP Header 还要带上一个值为 connected; type=analog 的头 Audio-Jack-Status。
C. RaopRtspOptionsHandler详解:
- 在iOS设备发起请求后,我们需要相应rtsp的option请求,以告知我们支持哪些类型的请求。
public class RaopRtspOptionsHandler extends SimpleChannelUpstreamHandler {
private static final String Options =
RaopRtspMethods.ANNOUNCE.getName() + ", " +
RaopRtspMethods.SETUP.getName() + ", " +
RaopRtspMethods.RECORD.getName() + ", " +
RaopRtspMethods.PAUSE.getName() + ", " +
RaopRtspMethods.FLUSH.getName() + ", " +
RtspMethods.TEARDOWN.getName() + ", " +
RaopRtspMethods.OPTIONS.getName() + ", " +
RaopRtspMethods.GET_PARAMETER.getName() + ", " +
RaopRtspMethods.SET_PARAMETER.getName();
@Override
public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent evt) throws Exception {
final HttpRequest req = (HttpRequest)evt.getMessage();
if (RtspMethods.OPTIONS.equals(req.getMethod())) {
final HttpResponse response = new DefaultHttpResponse(RtspVersions.RTSP_1_0, RtspResponseStatuses.OK);
response.setHeader(RtspHeaders.Names.PUBLIC, Options);
ctx.getChannel().write(response);
}
else {
super.messageReceived(ctx, evt);
}
}
}
3 运行第二部代码,代码见github链接
-
4 在iOS设备上通过AirTunes看到"RDuwan-Airtunes",点击就可以成功连接上。如图: