目的
在Anroid下完成tcp协议和udp协议,了解Http请求过程。
实现
VPNService类似创建一个虚拟网络,可以设置ip,设置应用,dns服务器。假设我们在自己app下访问http://example.com
,本地找不到域名映射就会请求dns拿ip(udp协议),拿到ip再进行tcp连接传输数据就完成http请求。假设我们启动VPNService服务,就可以拦截到udp数据包和tcp数据包,要想完成一个http请求,就得完成 udp和tcp协议。
完成udp协议:使用nio零拷贝,使用selector监听事件状态(减少线程),udp协议比较简单。
完成tcp协议:也使用nio和selector,tcp协议就比较麻烦,有三次握手和四次挥手。解析掉三次握手完,之后拿到数据,数据写入socket,socket读取到得数据再封装好数据包写入网络。socket已经帮完成tcp协议了,我们只需要交换数据。
遇到问题
VPNService
创建的ParcelFileDescriptor
需要nio(非阻塞)读取,nio不是异步所以需要线程池多线程执行(不然一个包解析到一个包多耗时),多线程就会导致数据包的乱序问题。- 多线程使用
Selector
问题,一个线程先selector.select();
(会阻塞线程),另一个线程后再执行channel.register(selector,...)
就会阻塞当前线程,可以先使用selector.wakeup()
唤醒第一个线程,再注册事件,大量数据执行还是会出现阻塞线程。 - 使用
Selector
监听所有socket的read事件,他是用一个线程去执行的,如何保证不同ip的socket的读取数据封装数据写到网络中互相不影响。 - 多线程使用FileChannel会出错。
最后效果
我是指定拦截com.android.browser
(自带浏览器)应用。

代码操作
使用 Android Studio 创建一个项目,创建一个MyVpnService继承VpnService
,setupVpn方法启动,builder设置基本参数,主要创建一个ParcelFileDescriptor
对象,这个对象可以拿到数据流,进行读写。
scss
if (descriptor == null) {
Builder builder = new Builder();
// 添加IPv4地址
builder.addAddress("10.0.0.2", 32);
// 添加IPv4路由,拦截所有ipv4
builder.addRoute("0.0.0.0", 0);
// 添加dns服务器
builder.addDnsServer("114.114.114.114");
try {
//指定应用拦截
builder.addAllowedApplication("com.example.vpnservicedemo");
//builder.addAllowedApplication("com.android.browser");
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
// ParcelFileDescriptor是一个文件描述符,它是一种程序读写已打开文件、socket的对象
descriptor = builder.setConfigureIntent(pendingIntent).establish();
//创建一个读取对象
readVpn = new ReadVpn(this, descriptor);
readVpn.start();
}
修改AndroidManifest.xml
文件,添加权限声明,MyVpnService
的service声明。
ini
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VpnServiceDemo"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MyVpnService" android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
</application>
</manifest>
ReadVpn的start方法,多线程读取数据并解析数据,pushPc.pushData()
是将数据包通过socket方式推送到pc电脑上,好让Wireshark查看数据包情况,parseData
解析数据包。
ini
public void start() {
//创建一个线程读取channel数据
readDataThread = new Thread(() -> {
FileChannel readChannel = null;
ExecutorService threadPool = null;
try {
//获取cpu核心数
int numOfCores = Runtime.getRuntime().availableProcessors();
//创建线程池
threadPool = Executors.newFixedThreadPool(numOfCores);
// 获取一个channel,零拷贝读取数据
readChannel = new FileInputStream(descriptor.getFileDescriptor()).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 20);
while (!Thread.interrupted()) {
int len = readChannel.read(buffer);
if (len == -1) {
break;
}
if (len > 0) {
buffer.flip();
byte[] bt = new byte[len];
buffer.get(bt);
//计算接收数量
receiveLen += len;
//发送到ui
pushUi();
threadPool.submit(() -> {
//推送PC
pushPc.pushData(bt);
parseData(bt);
});
buffer.clear();
}
}
} catch (Exception e) {
System.out.println("ReadData start 异常: " + e.getMessage());
} finally {
if (readChannel != null) {
try {
readChannel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (threadPool != null) {
threadPool.shutdown();
}
}
});
readDataThread.setName("readDataThread");
readDataThread.start();
}
parseData 方法,IpSelector.newPacket
解析数据包,根据源ip
和源prot
判断唯一,如果数据包是tcp就创建TcpPipe
,udp就创建UdpPipe
,然后再对应执行receive
方法。
ini
private void parseData(byte[] array) {
try {
Packet packet = IpSelector.newPacket(array, 0, array.length);
if (packet instanceof IpPacket) {
IpPacket ipPacket = (IpPacket) packet;
//通过源ip和源port判断唯一SendData
String key = getKey(ipPacket);
if (key != null) {
if (pipeMap.get(key) == null) {
Pipe pipe = null;
if (ipPacket.getHeader().getProtocol() == IpNumber.TCP) {
pipe = new TcpPipe(vpnService, this);
} else if (ipPacket.getHeader().getProtocol() == IpNumber.UDP) {
pipe = new UdpPipe(vpnService, this);
}
if (pipe != null) {
//防止多线程问题
pipeMap.putIfAbsent(key, pipe);
}
}
Pipe pipe = pipeMap.get(key);
int code = pipe.receive(ipPacket);
if (code == -1) {
//说明关闭了
pipeMap.remove(key);
}
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("ReadData parseData 解析包异常:" + e.getMessage());
}
}
TcpPipe.receive
方法,syn
标志(第一次握手)创建SocketChannel.open()
初始化属性,因为多线程原因,数据包乱序,所以通过序列号判断是不是可以发送的序号,不是就缓存到map,是的话发送,然后看看map里面还有没有可以发的,有就一起发送。
ini
public int receive(IpPacket ipPacket) throws IOException {
TcpPacket tcpPacket = (TcpPacket) ipPacket.getPayload();
boolean syn = tcpPacket.getHeader().getSyn();
boolean fin = tcpPacket.getHeader().getFin();
boolean psh = tcpPacket.getHeader().getPsh();
boolean ack = tcpPacket.getHeader().getAck();
boolean rst = tcpPacket.getHeader().getRst();
InetAddress dstAddr = ipPacket.getHeader().getDstAddr();
int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
int sequenceNumber = tcpPacket.getHeader().getSequenceNumber();
if (rst) {
close();
return -1;
}
if (sendFin.get() && replyFin.get()) {
close();
return -1;
}
if (syn) {
if (socketChannel == null) {
srcIpPacket = ipPacket;
replyAck.set(sequenceNumber + 1);
// 使用nio
socketChannel = SocketChannel.open();
// 使连接不被自身拦截
vpnService.protect(socketChannel.socket());
// 使用非阻塞io
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(dstAddr, dstPort));
//注册事件
TcpSelector.registerConnect(socketChannel, this);
}
} else {
if (socketChannel != null && !socketChannel.isConnected()) {
//连接重置
replyRst(tcpPacket);
return -1;
}
try {
Packet payload = tcpPacket.getPayload();
if ((payload != null && payload.getRawData().length > 0) || fin) {
//刚好到发送序列只发送
if (replyAck.get() == sequenceNumber) {
sendData(tcpPacket);
while (true) {
TcpPacket packets = dataMap.get(replyAck.get());
if (packets != null) {
dataMap.remove(replyAck.get());
sendData(packets);
} else {
break;
}
}
} else {
dataMap.put(sequenceNumber, tcpPacket);
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("sendData 异常:" + e.getMessage());
close();
return -1;
}
}
return 0;
}
TcpSelector
类监听SocketChannel
连接和读事件,因为selector.select()
是用一个线程执行,为了不让各个ip的构建数据包互相影响,所有推送到自己实例的队列中,队列又保证了顺序。可以看到while循环完,会读registerQueue
队列然后注册事件,这是为了解决多线程使用Selector
问题。
ini
public class TcpSelector {
private static Selector selector;
private static ConcurrentLinkedQueue<RegisterData> registerQueue = new ConcurrentLinkedQueue<>();
private static ExecutorService threadPool = Executors.newFixedThreadPool(5);
static {
try {
selector = Selector.open();
} catch (IOException e) {
System.out.println("TcpSelector 启动失败:" + e.getMessage());
}
Thread thread = new Thread(() -> {
try {
while (!Thread.interrupted()) {
// 等待事件
selector.select();
// 获取事件的迭代器
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
SocketChannel socketChannel = (SocketChannel) key.channel();
TcpPipe tcpPiping = (TcpPipe) key.attachment();
if (key.isConnectable()) {
try {
// 完成连接[这里会出现超时异常]
socketChannel.finishConnect();
//切换事件
key.interestOps(SelectionKey.OP_READ);
tcpPiping.replySyn();
} catch (Exception ex) {
ex.printStackTrace();
tcpPiping.close();
}
} else if (key.isReadable()) {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
//推送一个结束
tcpPiping.replyPshQueue.offer(null);
tcpPiping.close();
} else if (bytesRead > 0) {
buffer.flip();
byte[] array = new byte[buffer.limit()];
buffer.get(array);
tcpPiping.replyPshQueue.offer(array);
}
} catch (Exception e) {
tcpPiping.close();
e.printStackTrace();
}
}
// 删除处理过的事件
iterator.remove();
}
RegisterData registerData = null;
while ((registerData = registerQueue.poll()) != null) {
registerData.socketChannel.register(selector, SelectionKey.OP_CONNECT, registerData.tcpPipe);
}
}
} catch (Exception e) {
System.out.println("TcpSelector 线程异常结束:" + e.getMessage());
e.printStackTrace();
}
});
thread.setName("tcpSelector");
thread.start();
}
public static void registerConnect(SocketChannel socketChannel, TcpPipe tcpPipe) {
RegisterData registerData = new RegisterData();
registerData.socketChannel = socketChannel;
registerData.tcpPipe = tcpPipe;
registerQueue.offer(registerData);
selector.wakeup();
}
static class RegisterData {
SocketChannel socketChannel;
TcpPipe tcpPipe;
}
}