POSIX Sokcet API:Connectionless Communication

146 阅读6分钟

前一章中,通过POSIX Socket APIs完成了TCP协议面向连接的通信。本章中将会学习如何在Android应用和服务器之间建立一个无连接的通信。通过UDP的无连接通信可以向不在意数据包顺序和丢失的实时应用提供一个轻量级的通信介质。由于是无连接的,数据在传输过程中可能会发生错乱和丢失。本章将会和上一章一样完成UDP的客户端和服务端native实现。

新增Native UDP服务端

首先需要实现UDP服务端Activity:
package com.example.nativeexe.echo

import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.nativeexe.R

class EchoUDPServerActivity : AbsEchoActivity() {
    override fun getLayoutId(): Int = R.layout.activity_echo_server

    override fun onStartButtonClicked() {
        val port = getPort()
        if (port != -1) {
            val serverTask = ServerTask(port)
            serverTask.start()
        }
    }

    private inner class ServerTask(val port: Int) : AbsEchoTask() {
        override fun onBackground() {
            logMessage("Start Udp server...")

            try {

                nativeStartUdpServer(port)

            } catch (e: Exception) {
                logMessage(e.message.toString())
            }

            logMessage("Udp Server terminated.")
        }

    }

    external fun nativeStartUdpServer(port: Int)

}

接下来需要在Echo.cpp中实现UDP Server端代码。

实现Native UDP Server

新建UDP Server: socket

和创建TCP Server方法相同的方法也可以用于创建UDP Server。通过将stream socket替换成datagram socket来实现这一点:
static int NewUdpServer(JNIEnv *env, jobject obj) {
    LogMessage(env, obj, "Constructing a new UDP Socket...");
    //创建UDP socket
    int udpSocket = socket(PF_INET, SOCK_DGRAM, 0);
    //检查创建是否正常
    if (-1 == udpSocket) {
        ThrowErrnoException(env, "java/io/IOException", errno);
    }

    return udpSocket;
}

该方法创建了一个datagram socket返回一个socket descriptor。在创建错误的时候,返回-1并设置errno全局变量表示错误。socket创建完成后既可以发送和接受datagram。

从Socket接收Datagram:recvfrom

通过`recvfrom`方法可以从UDP socket中接收数据:
ssize_t recvfrom(int socketDescriptor, void* buffer, size_t bufferLength, 
 int flags, struct sockaddr* address, socklen_t* addressLength);

recv方法一样,recvfrom方法也是阻塞方法。如果socket中没有要被接收的数据,这个方法会将调用进程置于suspended状态,直到有数据可被接收。方法有6个入参:

  1. Socket descriptor: 表明想要接收数据的socket
  2. Buffer指针,指向接收数据的buffer内存地址
  3. Buffer 长度,表明buffer的size。recvfrom最多接收bufferLength个长度的数据就会返回
  4. Flags:表明接收数据额外的flag
  5. Address 指针,这里会被填充client端的协议地址。如果这个信息不需要,可以被设置为NULL
  6. Address 长度指针,指向5中协议Address在内存中的size。如果这个信息不需要,可以设置为NULL

如果recvfrom方法调用成功,返回从socket接收bytes的数量,否则返回-1和设置errno全局变量表示错误详情。在Echo.cpp中增加ReceiveDatagramFromSocket方法:


static ssize_t
ReceiveDatagramFromSocket(JNIEnv *env, jobject obj, int sd, struct sockaddr_in *address,
                          char *buffer, size_t bufferSize) {
    socklen_t addressLength = sizeof(sockaddr_in);

    //从socket中接收datagram
    LogMessage(env, obj, "Receiving from the socket...");
    ssize_t recvSize = recvfrom(sd, buffer, bufferSize, 0,
                                (struct sockaddr *) address, &addressLength);

    if (-1 == recvSize) {
        ThrowErrnoException(env, "java/io/IOException", errno);
    } else {
        //打印地址
        LogAddress(env, obj, "Received from", address);

        //截断buffer,这样可以生成一个string
        buffer[recvSize] = '\0';
        if (recvSize > 0) {
            LogMessage(env, obj, "Received %d bytes: %s", recvSize, buffer);
        }
    }

    return recvSize;
}

向Socket发送Datagram:sendto

通过`sendto`方法可以向UDP Socket发送数据:
ssize_t sendto(int socketDescriptor, const void* buffer, 
               size_t bufferSize, int flags, const struct sockaddr* address, 
               socklen_t addressLength);

sendto方法是一个阻塞方法,如果socket一直在传输数据,这个方法会将进程置于suspended状态,直到数据传输完毕,socket可以重新传输数据。这个方法有6个入参:

  1. Socket descriptor: 表明要发送数据的socket
  2. Buffer指针:指向发送数据的内存地址
  3. Buffer长度:表示buffer的size。sendto方法最多发送bufferSize长度的数据,然后返回
  4. Flags: 表示发送的额外flag
  5. Address:标识发送目标服务端的协议地址
  6. Address length:5中协议地址数据结构的size

如果发送操作成功,该方法返回发送数据的byte数。否则返回-1和errno全局变量,标识错误详情。在Echo.cpp中增加SendDatagramToSocket方法:

static ssize_t
SendDatagramToSocket(JNIEnv *env, jobject obj, int sd, const struct sockaddr_in *address,
                     const char *buffer, size_t bufferSize) {
    LogAddress(env, obj, "Sending to", address);
    ssize_t sentSize = sendto(sd, buffer, bufferSize, 0,
                              (const sockaddr *) address, sizeof(sockaddr_in));

    if (-1 == sentSize) {
        ThrowErrnoException(env, "java/io/IOException", errno);
    } else {
        if (sentSize > 0) {
            LogMessage(env, obj, "Sent %d bytes:%s", sentSize, buffer);
        }
    }

    return sentSize;
}

Native UDP Server方法

在`Echo.cpp`方法中增加`nativeStartUdpServer`方法:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativeexe_echo_EchoUDPServerActivity_nativeStartUdpServer(JNIEnv *env,
                                                                           jobject thiz,
                                                                           jint port) {
    int serverSocket = NewUdpServer(env, thiz);
    if (nullptr != env->ExceptionOccurred()) {
        BindSocketToPort(env, thiz, serverSocket, port);
        if (nullptr != env->ExceptionOccurred()) {
            goto exit;
        }

        if (0 == port) {
            GetSocketPort(env, thiz, serverSocket);
            if (nullptr != env->ExceptionOccurred()) {
                goto exit;
            }
        }

        struct sockaddr_in address;
        memset(&address, 0, sizeof(sockaddr_in));

        char buffer[MAX_BUFFER_SIZE];
        ssize_t recvSize;
        ssize_t sendSize;

        recvSize = ReceiveDatagramFromSocket(env, thiz, serverSocket, &address, buffer,
                                             MAX_BUFFER_SIZE);
        if (0 == recvSize || nullptr != env->ExceptionOccurred()) {
            goto exit;
        }

        sendSize = SendDatagramToSocket(env, thiz, serverSocket, &address, buffer, recvSize);
    }

    exit:
    if (serverSocket > 0) {
        close(serverSocket);
    }
}

新增Native UDP客户端

首先需要新增Client端Activity:
package com.example.nativeexe.echo

import android.os.Bundle
import android.widget.EditText
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.nativeexe.R

class EchoUDPClientActivity : AbsEchoActivity() {

    private lateinit var etIP: EditText
    private lateinit var etMessage: EditText

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        etIP = findViewById(R.id.et_ip)
        etMessage = findViewById(R.id.et_message)

    }

    override fun getLayoutId(): Int = R.layout.activity_echo_client

    override fun onStartButtonClicked() {
        val ip = etIP.text.toString()
        val port = getPort()
        val message = etMessage.text.toString()

        if (ip.isNotEmpty() && port != -1 && message.isNotEmpty()) {
            val clientTask = ClientTask(ip, port, message)
            clientTask.start()
        }
    }

    private inner class ClientTask(val ip: String, val port: Int, val message: String) :
    AbsEchoTask() {


        override fun onBackground() {
            logMessage("Start Client...")
            try {
                nativeStartUdpClient(ip, port, message)
            } catch (e: Exception) {
                logMessage(e.message.toString())
            }
            logMessage("Client terminated.")

        }

    }

    external fun nativeStartUdpClient(ip: String, port: Int, message: String)
}

实现Native UDP Client

在`Echo.cpp`中新增`nativeStartUdpClient`方法:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativeexe_echo_EchoUDPClientActivity_nativeStartUdpClient(JNIEnv *env,
                                                                           jobject thiz, jstring ip,
                                                                           jint port,
                                                                           jstring message) {
    int clientServer = NewUdpServer(env, thiz);
    if (nullptr != env->ExceptionOccurred()) {
        struct sockaddr_in address;
        memset(&address, 0, sizeof(sockaddr_in));
        address.sin_family = PF_INET;

        const char *ipAddress = env->GetStringUTFChars(ip, nullptr);
        if (nullptr == ipAddress) {
            goto exit;
        }

        int result = inet_aton(ipAddress, &(address.sin_addr));
        env->ReleaseStringUTFChars(ip, ipAddress);

        if (0 == result) {
            ThrowErrnoException(env, "java/io/IOException", errno);
            goto exit;
        }

        address.sin_port = htons(port);

        const char *messageText = env->GetStringUTFChars(message, nullptr);
        if (nullptr == messageText) {
            goto exit;
        }

        jsize messageSize = env->GetStringUTFLength(message);
        SendDatagramToSocket(env, thiz, clientServer, &address, messageText, messageSize);
        env->ReleaseStringUTFChars(message, messageText);

        if (nullptr != env->ExceptionOccurred()) {
            goto exit;
        }

        char buffer[MAX_BUFFER_SIZE];
        memset(&address, 0, sizeof(address));
        ReceiveDatagramFromSocket(env, thiz, clientServer, &address, buffer, MAX_BUFFER_SIZE);
    }

    exit:
    if (clientServer > 0) {
        close(clientServer);
    }
}

运行UDP Socket示例工程

和之前TCP的示例一样,需要在两个Android模拟器中分别运行Client端和Server端。将Server端的端口号设置为0,一旦Server运行起来之后,记录下自动分配的端口号。

连接模拟器的UDP

需要通过ADB设置UDP端口转发实现两台模拟器UDP通信:
  1. 记录下模拟器四位数表示的代号(例如5556)
  2. 使用terminal连接到localhost:port
  3. 在打开的命令行中,运行如下命令:
 redir add udp:<emulator port number>:<host port number>

实现模拟器端口到宿主设备的端口转发。任何连接到宿主设备端口上的连接都会被转发到模拟器的端口上。这样的设置是运行时的,一旦虚拟机重启,配置就会被清除。

运行Echo UDP Client

和之前TCP配置相似,通过ip 10.0.2.2和之前设置的host port number,就可以启动Client端。

总结

本章中主要学习了POSIX Socket APIs实现无连接通信。通过对Client和Server端UDP协议的实现,可以体会到UDP通信的关键点。接下来一章,将会学习如何在同一个设备上的两个应用之间通信。