POSIX Socket API:Local Communication

188 阅读5分钟

前两章中学习了使用POSIX Socket APIs实现远程设备通信。POSIX Socket APIs也能实现同一台设备两个应用之间的通信,或者在native和Java之间通信。本章中将会继续完善Echo示例工程,实现如下功能:

  1. 在native端实现Local Socket Server
  2. 在Java端实现Local Socket Client
  3. 在两个应用间建立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下创建:

  1. Abstract namespace: 通过local socket通信协议模块维护。socket name在绑定socket name之前加一个NULL字符前缀
  2. 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个入参:

  1. nfds:表明需要监听的描述符中最高的描述符+1,select方法将会监听包括这个描述符在内的描述符。
  2. readfds:可读监听描述符集合
  3. writefs:可写监听描述符集合
  4. exceptfds: 错误监听描述符集合
  5. timeout:select方法阻塞时间,如果不需要,可以设置为NULL,表示永远阻塞

如果这个方法调用成功,该方法返回处于ready的描述符数量,否则返回-1并设置errno全局变量。

描述符集合通过fd_set方法提供:

 struct fd_set readfds;

为了操作描述符集合,提供了如下宏:

  1. FD_ZERO宏:传入一个fd_set指针,并清空
  2. FD_SET宏:传入一个fd_set指针,可以添加一个描述符
  3. FD_CLR宏:传入一个fd_set指针,可以移除一个描述符
  4. FD_ISSET宏:用于在select方法调用结束后检查某个描述符是否在select方法的返回值中

总结

本章中主要学习了POSIX Socket APIs在同一个设备上创建Local Socket。同时简单过了一下异步I/O。