目的
在Anroid下完成TCP协议和UDP协议,了解HTTP请求过程。
请求流程
应用HTTP请求流程
下面是客户端发起HTTP请求大概流程,这里的虚拟网络
是通过VPNService
创建,HTTP
的数据包都会写入虚拟网络,最后会根据IP和端口从虚拟网络
拿到响应数据,这些操作都是应用那边的,我们不需要管,我们只要知道,他把HTTP
请求的数据包写入虚拟网络
,我们可以读出来,然后通过Socket
连接写入数据,再读取Socket
数据,按照规定协议写入虚拟网络
中,应用他会自己读取出来。
JAVA应用流程
下面是JAVA应用大概流程,通过虚拟网络
获取数据包,然后通过pcap4j
解析数据包,通过源ip
和源port
从Map
中获取TCP
或UDP
连接,根据协议不同走不同流程。TCP
使用SocketChannel
和UDP
使用DatagramChannel
两个NIO
,设置非阻塞
通过Selector
去监听事件。
TCP协议
中的SEQ
计算
操作 | 计算 |
---|---|
回应SYN | + 1 |
回应FIN | + 1 |
回应带data | + data.length |
其他 | + 0 |
遇到问题
- 多线程使用
Selector
问题,一个线程先selector.select();
(会阻塞线程),另一个线程后再执行channel.register(selector,...)
就会阻塞当前线程,可以先使用selector.wakeup()
唤醒第一个线程,再注册事件,大量数据执行还是会出现阻塞线程。 - 多线程使用FileChannel会出错。
- channel.write(buffer)写入数据不一定将buffer的数据写完。
最后效果
我是指定拦截com.android.browser
(自带浏览器)应用。
代码操作
使用 Android Studio 创建一个项目,创建一个MyVpnService继承VpnService
,setupVpn方法启动,builder设置基本参数,主要创建一个ParcelFileDescriptor
对象,这个对象可以拿到数据流,进行读写。
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声明。
<?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
解析数据包。
public void start() {
//创建一个线程读取channel数据
readDataThread = new Thread(() -> {
FileChannel readChannel = null;
ExecutorService threadPool = null;
try {
// 获取一个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();
//推送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
方法。
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()
初始化属性,注册连接事件到Selecter
,非syn
标志有数据就写入SocketChannel
,有fin
标志就回应fin
标志,并且关闭SocketChannel
。
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) {
int dataLen = 0;
if (payload != null && payload.getRawData().length > 0) {
dataLen = payload.getRawData().length;
}
if (dataLen > 0) {
if (socketChannel.isConnected()) {
ByteBuffer wrap = ByteBuffer.wrap(payload.getRawData());
while (wrap.hasRemaining()){
socketChannel.write(wrap);
}
}
}
int add = dataLen + (fin ? 1 : 0);
replyAck(replyAck.get() + add);
replyAck.addAndGet(add);
if (tcpPacket.getHeader().getFin()) {
replyFin.set(true);
if (sendFin.getAndSet(true)) {
replyFin();
}
socketChannel.close();
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("sendData 异常:" + e.getMessage());
close();
return -1;
}
}
return 0;
}