前两章中学习了使用POSIX Socket APIs实现远程设备通信。POSIX Socket APIs也能实现同一台设备两个应用之间的通信,或者在native和Java之间通信。本章中将会继续完善Echo示例工程,实现如下功能:
- 在native端实现Local Socket Server
- 在Java端实现Local Socket Client
- 在两个应用间建立local socket通信
Echo Local Activity
布局仍然复用之前TCP Server页面的布局。Activity中的代码如下:package com.example.nativeexe.echo.local
import android.net.LocalSocket
import android.net.LocalSocketAddress
import com.example.nativeexe.R
import com.example.nativeexe.echo.AbsEchoActivity
import java.io.File
class EchoLocalServerActivity : AbsEchoActivity() {
override fun getLayoutId(): Int = R.layout.activity_echo_server
override fun onStartButtonClicked() {
val name = etPort.text.toString()
val message = "onStartButtonClicked:${Math.random()}"
if (name.isNotEmpty()) {
val socketName = if (isFileSystemSocket(name)) {
val file = File(filesDir, name)
file.absolutePath
} else {
name
}
val serverTask = ServerTask(socketName)
serverTask.start()
val clientTask = ClientTask(socketName, message)
clientTask.start()
}
}
private fun isFileSystemSocket(port: String): Boolean {
return port.startsWith("/")
}
private inner class ServerTask(val socketName: String) : AbsEchoTask() {
override fun onBackground() {
logMessage("Start server...")
try {
nativeStartLocalServer(socketName)
} catch (e: Exception) {
logMessage(e.message.toString())
}
logMessage("Server terminated.")
}
}
private inner class ClientTask(val socketName: String, val message: String) : AbsEchoTask() {
override fun onBackground() {
logMessage("Starting Client...")
try {
startLocalClient(socketName, message)
} catch (e: Exception) {
logMessage(e.message.toString())
}
logMessage("Client terminated.")
}
}
@Throws(Exception::class)
private fun startLocalClient(socketName: String, message: String) {
val clientSocket = LocalSocket()
try {
val namespace = if (isFileSystemSocket(socketName)) {
LocalSocketAddress.Namespace.FILESYSTEM
} else {
LocalSocketAddress.Namespace.ABSTRACT
}
val localSocketAddress = LocalSocketAddress(socketName, namespace)
logMessage("Connecting to $socketName")
clientSocket.connect(localSocketAddress)
logMessage("Connected!")
val messageBytes = message.encodeToByteArray()
logMessage("Sending to the socket...")
val outputStream = clientSocket.outputStream
outputStream.write(messageBytes)
logMessage("Send ${messageBytes.size} bytes:$message")
val inputStream = clientSocket.inputStream
val readSize = inputStream.read(messageBytes)
val receivedMsg = String(messageBytes, 0, readSize)
logMessage("Received $readSize bytes:$receivedMsg")
outputStream.close()
inputStream.close()
} finally {
clientSocket.close()
}
}
external fun nativeStartLocalServer(socketName: String)
}
代码创建了两个线程,一个用于创建Server一个用于创建Client。
实现native Local Socket Server
创建Local Server: socket
和之前创建TCP/UDP socket方法相同,可以通过`socket`方法创建传入`PF_LOCAL`参数创建Local Socket。在`Echo.cpp`方法中新增`NewLocalSocket`方法:static int NewLocalSocket(JNIEnv *env, jobject obj) {
LogMessage(env, obj, "Constructing a new local UNIX socket...");
int localServer = socket(PF_LOCAL, SOCK_STREAM, 0);
if (-1 == localServer) {
ThrowErrnoException(env, "java/io/IOException", errno);
}
return localServer;
}
绑定Socket到Name: bind
和TCP/UDP相似,Socket创建完成之后是没有protocal address的。通过`bind`方法可以将local socket绑定到提供的Name上。Local Socket的protocal address是`sockaddr_un`类型:struct sockaddr_un {
sa_family_t sun_family;
char sun_path[UNIX_PATH_MAX];
};
local address的protocal address由一个name组成。不包含IP地址或者端口号。Local Socket可以在两种不同的namespace下创建:
- Abstract namespace: 通过local socket通信协议模块维护。socket name在绑定socket name之前加一个NULL字符前缀
- Filesystem namespace:通过将文件系统作为一个特殊的socket文件维护。socket name直接传递给sockaddr_un结构体来将socket name绑定到socket
在Echo.cpp中新增BindSocketToName方法:
static void BindLocalSocketToName(JNIEnv *env, jobject obj, int sd, const char *name) {
struct sockaddr_un address;
//socket 名称的长度
const size_t nameLength = strlen(name);
//将path 长度初始化为等于name的长度
size_t pathLength = nameLength;
//如果name以/开头,是一个abstract namespace
bool abstractNamespace = ('/' != name[0]);
//abstract namespace 需要将path的第一个byte设置为0byte,所以需要将长度+1
if (abstractNamespace) {
pathLength++;
}
//检查path长度
if (pathLength > sizeof(address.sun_path)) {
ThrowErrnoException(env, "java/io/IOException", errno);
} else {
//清除address bytes
memset(&address, 0, sizeof(address));
address.sun_family = PF_LOCAL;
//Socket path
char *sunPath = address.sun_path;
//abstract namespace第一个byte必须设置为0
if (abstractNamespace) {
*sunPath++ = '\0';
}
//append the local name
strcpy(sunPath, name);
//地址长度
socklen_t addressLength = (offsetof(struct sockaddr_un, sun_path)) + pathLength;
//如果socket已经bind了,unlink
unlink(address.sun_path);
//绑定socket
LogMessage(env, obj, "Binding to local name %s%s.", abstractNamespace ? "(null)" : "",
name);
if (-1 == bind(sd, (struct sockaddr *) &address, addressLength)) {
ThrowErrnoException(env, "java/io/IOException", errno);
}
}
}
BindLocalSocketToNamenative方法将local socket绑定到指定的name上。这个方法里面会检查local socket name是不是以/开头,来确定是否是abstract和file system namespace。bind成功后,应用可以开始等待连接。
接受Local Socket:accept
Local socket通过`accpet`方法来监听潜在的连接,和client端`accept`方法不同的点在于返回`socketaddr_un`类型。在`Echo.cpp`方法中增加`AcceptOnLocalSocket`方法:static int AcceptOnLocalSocket(JNIEnv *env, jobject obj, int sd) {
LogMessage(env, obj, "Waiting for a client connection...");
int clientSocket = accept(sd, nullptr, nullptr);
if (-1 == clientSocket) {
ThrowErrnoException(env, "java/io/IOException", errno);
}
return clientSocket;
}
Native Local Socket Server
在`Echo.cpp`中增加`nativeStartLocalServer`方法:extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativeexe_echo_local_EchoLocalServerActivity_nativeStartLocalServer(JNIEnv *env,
jobject thiz,
jstring socket_name) {
int serverSocket = NewLocalSocket(env, thiz);
if (nullptr != env->ExceptionOccurred()) {
const char *nameText = env->GetStringUTFChars(socket_name, nullptr);
if (nullptr == nameText) {
goto exit;
}
BindLocalSocketToName(env, thiz, serverSocket, nameText);
env->ReleaseStringUTFChars(socket_name, nameText);
if (nullptr != env->ExceptionOccurred()) {
goto exit;
}
ListenOnSocket(env, thiz, serverSocket, 4);
if (nullptr != env->ExceptionOccurred()) {
goto exit;
}
int clientSocket = AcceptOnLocalSocket(env, thiz, serverSocket);
if (nullptr != env->ExceptionOccurred()) {
goto exit;
}
char buffer[MAX_BUFFER_SIZE];
ssize_t recvSize, sentSize;
while (1) {
recvSize = ReceiveFromSocket(env, thiz, serverSocket, buffer, MAX_BUFFER_SIZE);
if ((0 == recvSize) || nullptr != env->ExceptionOccurred()) {
break;
}
sentSize = SendToSocket(env, thiz, serverSocket, buffer, recvSize);
if (sentSize == 0 || nullptr != env->ExceptionOccurred()) {
break;
}
}
close(clientSocket);
}
exit:
if (serverSocket > 0) {
close(serverSocket);
}
}
异步I/O
上面几章中的Socket APIs都是阻塞型方法。这些方法会阻塞调用流程,直到满足某种预设的条件,例如在socket中有数据。通过`select`方法可以实现异步I/O。可其他socket APIs一次只能操作一个socket,`select`方法可以接受多个socket descriptor并且同时监听他们的状态。这个方法会block直到某个预设的条件或者timeout。通过如下代码使用select:
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds,
fd_set* exceptfds, struct timeval* timeout);
该方法接受5个入参:
- nfds:表明需要监听的描述符中最高的描述符+1,
select方法将会监听包括这个描述符在内的描述符。 - readfds:可读监听描述符集合
- writefs:可写监听描述符集合
- exceptfds: 错误监听描述符集合
- timeout:select方法阻塞时间,如果不需要,可以设置为NULL,表示永远阻塞
如果这个方法调用成功,该方法返回处于ready的描述符数量,否则返回-1并设置errno全局变量。
描述符集合通过fd_set方法提供:
struct fd_set readfds;
为了操作描述符集合,提供了如下宏:
- FD_ZERO宏:传入一个
fd_set指针,并清空 - FD_SET宏:传入一个
fd_set指针,可以添加一个描述符 - FD_CLR宏:传入一个
fd_set指针,可以移除一个描述符 - FD_ISSET宏:用于在select方法调用结束后检查某个描述符是否在select方法的返回值中