POSIX Socket API:Connection-Oriented Communication

181 阅读17分钟

native代码运行在单独的环境中,但是需要和主应用通信或者向外部提供服务。在前面的章节中通过JNI的方式可以和Java主应用通信。在本章中,将会使用Bionic中POSIX Socket API来让native代码直接和外部通信而无需通过Java层。

Socket是一个通过名称和地址链接链接节点在应用间传输数据的连接方式。这些应用可以在同一台设备上也可以在网络中其他设备上。POSIX Socket API(之前被称为Berkeley Sokcet API),是一个可以通过各种协议传输数据高度灵活的API。

本章将会简单介绍POSIX Socket API和Android平台有关的部分:

  1. POSIX Socket整体介绍
  2. Socket 家族
  3. 面向连接的socket

在详细深入面向连接通信的POSIX socket API之前,需要创建一个间的名为Echo的应用。这个应用将会作为一个实验台来帮助了解socket编程的方方面面。

Echo Socket示例工程

工程会包含如下内容:
  1. 一个配置socket必要参数的简单UI
  2. 一个简单的echo 服务,用于向发送者返回发送内容
  3. 简单的样板代码用于在native完成socket编程
  4. 一个面向连接的socket通信例子
  5. 一个无链接socket通信示例
  6. 一个本地socket通信示例

Echo Android应用工程

首先分别创建部分代码:
package com.example.nativeexe.echo

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ScrollView
import android.widget.TextView
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import com.example.nativeexe.R

abstract class AbsEchoActivity : AppCompatActivity() {

    companion object {
        // Used to load the 'echo' library on application startup.
        init {
            System.loadLibrary("echo")
        }
    }

    protected lateinit var etPort: EditText
    protected lateinit var btnStart: Button
    protected lateinit var svLog: ScrollView
    protected lateinit var tvLog: TextView

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

        etPort = findViewById(R.id.et_port)
        btnStart = findViewById(R.id.btn_start)
        svLog = findViewById(R.id.sv_log)
        tvLog = findViewById(R.id.tv_log)

        btnStart.setOnClickListener {
            onStartButtonClicked()
        }
    }

    protected fun getPort(): Int {

        var port: Int = -1

        try {
            port = Integer.parseInt(etPort.text.toString())
        } catch (e: Exception) {
            e.printStackTrace()
        }

        return port
    }

    protected fun logMessage(message: String) {
        runOnUiThread {
            logMessageDirect(message)
        }
    }

    private fun logMessageDirect(message: String) {
        tvLog.append(message)
        tvLog.append("\n")
        svLog.fullScroll(View.FOCUS_DOWN)
    }

    @LayoutRes
    abstract fun getLayoutId(): Int

    abstract fun onStartButtonClicked()

    protected abstract inner class AbsEchoTask : Thread() {
        private val handler by lazy {
            Handler(Looper.getMainLooper())
        }

        protected fun onPreExecute() {
            btnStart.isEnabled = false
            tvLog.text = ""
        }

        @Synchronized
        public override fun start() {
            onPreExecute()
            super.start()
        }

        override fun run() {
            onBackground()
            handler.post {
                onPostExecute()
            }
        }

        protected fun onPostExecute() {
            btnStart.isEnabled = false

        }

        abstract fun onBackground()
    }
}


#include <jni.h>
#include <string>

//
// Created by Jenry Sun on 2024/9/18.
//

#define MAX_LOG_MESSAGE_LENGTH 256


extern "C"
    JNIEXPORT jstring JNICALL
Java_com_example_nativeexe_EchoActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
    std::string hello = "Hello from C++ Echo";
    return env->NewStringUTF(hello.c_str());
}

static void LogMessage(JNIEnv *env, jobject obj, const char *format, ...) {
    //缓存log method id
    static jmethodID methodId = nullptr;
    if (nullptr == methodId) {
        jclass clazz = env->GetObjectClass(obj);
        methodId = env->GetMethodID(clazz, "logMessage", "(Ljava/lang/String;)V");
        env->DeleteLocalRef(clazz);
    }

    if (nullptr != methodId) {
        //格式化log信息
        char buffer[MAX_LOG_MESSAGE_LENGTH];
        //va_list用于处理可选参数
        va_list ap;
        //使ap指向第一个可选参数
        va_start(ap, format);
        //将格式化数据从可选参数写入缓冲区
        vsnprintf(buffer, MAX_LOG_MESSAGE_LENGTH, format, ap);
        //清空参数列表,并使ap无效
        va_end(ap);

        //将buffer转化为Java String
        jstring message = env->NewStringUTF(buffer);
        if (nullptr != message) {
            //log message
            env->CallVoidMethod(obj, methodId, message);
            //释放对message引用
            env->DeleteLocalRef(message);
        }
    }
}

static void ThrowException(JNIEnv *env, const char *className, const char *message) {
    jclass clazz = env->FindClass(className);
    if (nullptr != clazz) {
        env->ThrowNew(clazz, message);
        env->DeleteLocalRef(clazz);
    }
}

/**
 * 根据error number抛出异常及error message
 *
 * @param env
 * @param className
 * @param errnum
 */
static void ThrowErrnoException(JNIEnv *env, const char *className, int errnum) {
    char buffer[MAX_LOG_MESSAGE_LENGTH];
    if (-1 == strerror_r(errnum, buffer, MAX_LOG_MESSAGE_LENGTH)) {
        strerror_r(errno, buffer, MAX_LOG_MESSAGE_LENGTH);
    }
    ThrowException(env, className, buffer);
}

使用TCP Socket完成面向连接的通信

面向连接的TCP Socket提供了健壮、容错的通信中介。这种连接在通信的整个周期中都保持开启,自动帮应用处理收到数据包的排序和错误检查。下面将会修改TCP server和client Activity来演示如何使用socket建立连接和交换数据。

Server端

Echo Server Activity Layout

首先需要新建一个Server Activity页面的xml activity_echo_server :
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/main"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".echo.EchoServerActivity">

  <EditText
    android:id="@id/et_port"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="50dp"
    android:hint="@string/port_edit"
    android:importantForAutofill="no"
    android:inputType="number"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:ignore="ExtraText,LabelFor,TextFields" />

  <Button
    android:id="@id/btn_start"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/start_server_button"
    app:layout_constraintEnd_toEndOf="@id/et_port"
    app:layout_constraintStart_toStartOf="@id/et_port"
    app:layout_constraintTop_toBottomOf="@id/et_port" />

  <ScrollView
    android:id="@id/sv_log"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_marginTop="16dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/btn_start">

    <TextView
      android:id="@id/tv_log"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />

  </ScrollView>


</androidx.constraintlayout.widget.ConstraintLayout>

Echo Server Activity

```kotlin package com.example.nativeexe.echo

import com.example.nativeexe.R

class EchoServerActivity : 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 server...")

        try {

            nativeStartTcpServer(port)

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

        logMessage("Server terminated.")
    }

}

external fun nativeStartTcpServer(port: Int)

}


<h3 id="ycYw3">实现Native TCP Server</h3>
接下来就需要native代码中创建TCP Server。

<h4 id="VsBdM">创建Socket:socket</h4>
socket通过socket descriptor(socket描述符)表示。除了创建Socket的API外,其他Socket的API都需要一个传入一个socket描述符。Socket可以通过`socket` 方法创建:

```c
int socket(int domain, int type, int protocol);

这个方法接收三个入参:

  1. domian 表示了Socket将要发生domain 和协议族。在写本章时,Android 平台提供了如下协议族:
    1. PF_LOCAL:主机内部通信协议族。这个协议族允许运行在同一个物理设备上的应用使用Socket API进行通信
    2. PF_INET:Internet version 4 协议族。这个协议族允许应用通过网络进行通信
  2. Type 指明了通信的语义。支持的类型如下:
    1. SOCKET_STREAM: Stream socket类型使用TCP协议提供了面向连接的通信
    2. SOCKET_DGRAM: Datagram socket类型使用UDP协议提供了无连接的通信
  3. Protocal:指明了将会被使用的协议。对于大多数协议族和类型,只能使用一种类型的协议。如果为0,则使用默认的通信协议。

如果Socket创建成功,这个方法返回关联的socket descriptor。否则返回-1和errno 全局变量来表示发生错误。

Echo.cpp 中新增NewTcpSocket方法来创建socket:

static int NewTcpSocket(JNIEnv *env, jobject obj) {
    LogMessage(env, obj, "Constructing a new TCP socket...");
    int tcpSocket = socket(PF_INET, SOCK_STREAM, 0);
    if (-1 == tcpSocket) {
        ThrowErrnoException(env, "java/io/Exception", errno);
    }

    return tcpSocket;
}
通过bind将Socket绑定到一个具体的地址
当通过socket方法创建一个socket时,只有socket族空间而没有协议地址。为了让客户端定位和连接上这个socket,首先需要绑定一个地址。Socket可以通过`bind` 方法绑定到一个地址上:
int bind(int socketDescriptor, const struct sockaddr* address, 
         socklen_t addressLength);

这个方法有三个入参:

  1. socket descriptor表明将要被绑定到socket
  2. address表明socket将要被绑定到的协议地址
  3. address length表明传递给这个方法的协议地址结构体(就是2中的结构体)的具体size

根据协议族的不同,需要提供不同的协议地址。对于PF_INET 协议族,使用socketaddr_in 结构体来表示具体的协议地址。sockaddr_in 结构体定义如下:

struct sockaddr_in { 
    sa_family_t sin_family; 
    unsigned short int sin_port; 
    struct in_addr sin_addr;
}

如果接口bind方法绑定正常,返回0。否则返回-1并设置errno全局变量来表示具体的错误。

接下来就需要在Echo.cpp 中添加BindSocketToPort 方法:

static void BindSocketToPort(JNIEnv *env, jobject obj, int sd, unsigned short port) {
    struct sockaddr_in address;
    //绑定socket的address
    memset(&address, 0, sizeof(address));
    address.sin_family = PF_INET;
    //绑定到所有的地址
    address.sin_addr.s_addr = htonl(INADDR_ANY);
    //将port转化为网络字节顺序
    address.sin_port = htons(port);

    //bind socket
    LogMessage(env, obj, "Binding to port %hu.", port);
    if (-1 == bind(sd, (struct sockaddr *) &address, sizeof(address))) {
        //抛出异常
        ThrowErrnoException(env, "java/io/IOException", errno);
    }
}

如果address结构体中port的值是0,bind方法将会找到第一个可以被用于socket的端口。端口值随后可以通过getsockname 方法获取。可以在Echo.cpp 中增加GetSocketPort方法:

static unsigned short GetSocketPort(JNIEnv *env, jobject obj, int sd) {
    unsigned short port = 0;
    struct sockaddr_in address;
    socklen_t addressLength = sizeof(sockaddr_in);
    //获取Socket address
    if (-1 == getsockname(sd, (struct sockaddr *) &address, &addressLength)) {
        ThrowErrnoException(env, "java/io/IOException", errno);
    } else {
        //将port转化为宿主字节形式
        port = ntohs(address.sin_port);
        LogMessage(env, obj, "Binded to random port: %hu.", port);
    }

    return port;
}

正如ntohs() 方法所展现的那样,端口并不是直接从sockaddr_in中获取的。需要通过nthos() 方法做一个转换。这是因为端口在主机和网络中表示的字节顺序是不一样的。

网络字节顺序
不同的架构设备使用不同的字节顺序在硬件层表示数据 。这就是设备的字节顺序,或者端。例如:
  1. Big-endian,大端字节顺序。按字节从高到低位存储
  2. Little-endian,小端字节存储。按字节从低到高位存储

不同字节顺序的设备不能够直接交换数据。为了让不同字节顺序的设备能够在网络中交换数据,网络协议规定大端字节顺序作为交换数据的规定。

因为JVM已经使用了大端字节顺序,Java应用无需做任何处理就可以直接在网络中交换数据。与之相对,native代码部分使用的是设备字节顺序。

  1. ARM 和x86架构使用小端字节顺序
  2. MIPS架构使用大端字节顺序

natvie代码在网络中交换数据的时候需要做适当的转换,将设备字节顺序转换为网络字节顺序。

socket库提供了一系列方法用于native代码转换字节顺序。这些方法通过sys/endian.h 头文件提供。

#include <sys/endian.h>

这个头文件会提供一下方法:

  1. htons 将unsigned short从宿主字节顺序转换为网络字节顺序
  2. nthoshtons相反,将unsigned short从网络字节顺序转化为宿主字节顺序
  3. htonl 将unsinged integer从宿主字节顺序转换为网络字节顺序
  4. ntohlhtonl相反,将unsinged integer从网络字节顺序转换为宿主字节顺序

使用这些工具类方法非常方便,因为这些方法在编译期间根据目标设备的架构确定了转换的规则。如果宿主字节顺序和网络字节顺序不同,这个方法会做相应的转换,否则不做任何处理。本章后续将会经常使用这些方法。

监听连接:listen
通过`listen` 方法socket可以监听连接:
int listen(int socketDescriptor, int backlog);

该方法有两个入参:

  1. socket descriptor: 指明了需要监听连接的socket实例对象
  2. backlog:指明了最大的等待连接数。如果socket应用正在处理来自客户端的链接,其他客户端的连接请求就会作为将要连接的请求存储在一个Queue中,这个Queue的大小限制为backlog。如果到达了backlog限制大小,后续的请求将会被拒绝。

如果这个方法调用成功,返回0,否则返回-1和设置errno全局变量用于表示错误。在Echo.cpp中增加ListenOnSocket 方法:

static void ListenOnSocket(JNIEnv *env, jobject obj, int sd, int backlog) {
    LogMessage(env, obj, "Listening on socket with a backlog of %d pending connections.", backlog);
    if (-1 == listen(sd, backlog)) {
        ThrowErrnoException(env, "java/io/Exception", errno);
    }
}

listen方法将将要发生的连接放到queue中,然后等待应用处理。

处理连接:accept
`accept`方法用于讲一个连接从listen queue中取出来,然后处理它:
int accept(int socketDescriptor, struct sockaddr* address, 
           socklen_t* addressLength);

accept 是一个阻塞方法。如果listen queue中没有要处理的连接,这个方法会将调用程序置于suspended状态直到新的连接达到。accept方法有三个入参:

  1. socket descriptor:表明要处理的socket
  2. 指向address 结构体的指针,这个结构体里有连接客户端的协议地址。如果服务端应用不需要这个信息,可以设置为NULL
  3. address长度指针,即2中的结构体长度。如果服务端应用不需要这个信息,可以设置为NULL

如果accept 请求成功,这个方法会返回客户端socket descriptor用来和连接客户端交互。否则返回-1和设置errno全局变量来表示错误。

在示例应用中,将会获取连接客户端的信息,并将信息显示到Activity上。在Echo.cpp中增加LogAddress 方法,该方法用于获取显示必要的信息:

static void LogAddress(JNIEnv *env, jobject object, const char *message,
                       const struct sockaddr_in *address) {
    char ip[INET_ADDRSTRLEN];
    //将IP地址转化为String
    if (nullptr == inet_ntop(PF_INET, &(address->sin_addr), ip, INET_ADDRSTRLEN)) {
        ThrowErrnoException(env, "java/io/Exception", errno);
    } else {
        //将port转化为host byte order
        unsigned short port = ntohs(address->sin_port);
        LogMessage(env, object, "%s %s:%hu.", message, ip, port);
    }
}

Echo.cpp中增加AcceptOnSocket 方法用于处理连接:

static int AcceptOnSocket(JNIEnv *env, jobject obj, int sd) {
    struct sockaddr_in address;
    socklen_t addressLength = sizeof(sockaddr_in);

    //block 然后等待client连接,然后处理连接
    LogMessage(env, obj, "Waiting for a client connection...");

    int clientSocket = accept(sd, (struct sockaddr *) &address, &addressLength);

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

    return clientSocket;

}

一旦accept了一个连接,accept 方法返回新的socket descriptor,可以用于和客户端交换数据。

从Socket接收数据:recv
通过`recv`方法可以从socket中接收数据:
ssize_t recv(int socketDescriptor, void* buffer, size_t bufferLength, int flags);

recv方法也是阻塞的。如果socket没有收到数据,这个方法会将调用的进程置于suspended状态,直到新的数据传输过来。这个方法有四个入参:

  1. socket descriptor:用于表明想要接收数据的socket
  2. 指向内存地址的buffer指针,用于从socket中接收数据
  3. buffer length,表明buffer的size。recv方法只会填充满bufferLength后才会返回
  4. Flags,表明接收数据flag

如果recv方法调用成功,返回从socket中接受的字节数量,否则返回-1和设置errno全局变量表示错误。如果方法返回0,表示socket连接断开。在Echo.cpp 方法中增加ReceiveFromSocket方法:

static ssize_t ReceiveFromSocket(JNIEnv *env, jobject obj, int sd, 
                                char *buffer, size_t bufferSize) {
    //block 然后从socket中接收数据到buffer中
    LogMessage(env, obj, "Receiving from the socket...");
    ssize_t recvSize = recv(sd, buffer, bufferSize - 1, 0);

    if (-1 == recvSize) {
        ThrowErrnoException(env, "java/io/IOException", errno);
    } else {
        //NULL截断buffer,这样可以生成一个string
        buffer[recvSize] = NULL;
        //如果接收到数据
        if (recvSize > 0) {
            LogMessage(env, obj, "Receive &d bytes:%s", recvSize, buffer);
        } else {
            LogMessage(env, obj, "Client disconnected.");;;
        }
    }
    
    return recvSize;
}

ReceiveFromSocket方法使用recv方法从socket中接收数据到buffer中。如果有错误,抛出IOException。通过Socket发送数据方式是类似的。

向Socket发送数据:send
通过`send`方法向Socket中发送数据:
ssize_t send(int socketDescriptor, void* buffer, size_t bufferLength, int flags);

recv方法一样,send方法也是一个阻塞方法。如果socket一直在发送数据中,这个方法会将调用进程置为suspended状态直到socket能够再次传送数据。send方法有四个入参:

  1. socket descriptor:表明想要向哪个socket中发送数据
  2. 指向内存地址的buffer指针,socket会将这个内存中的数据发送出去
  3. buffer length标明了buffer的size。send方法只会发送buffer length长度的数据然后返回
  4. Flags表明发送的额外flag

如果发送成功,send方法返回发送数据的byte数量。否则返回-1并且设置errno全局变量表示错误。和recv方法相似,如果这个方法返回0,表明socket断开连接。在Echo.cpp中增加SendToSocket方法:

static ssize_t SendToSocket(JNIEnv *env, jobject obj, int sd,
                            const char *buffer, size_t bufferSize) {
    //向socket发送数据
    LogMessage(env, obj, "Sending to socket...");
    ssize_t sentSize = send(sd, buffer, bufferSize, 0);
    if (-1 == sentSize) {
        ThrowErrnoException(env, "java/io/IOException", errno);
    } else {
        if (sentSize > 0) {
            LogMessage(env, obj, "Send %d bytes: %s", sentSize, buffer);
        } else {
            LogMessage(env, obj, "Client disconnected.");
        }
    }

    return sentSize;
}

SendToSocket使用send方法通过buffer向socket发送数据。如果发生错误会抛出IOException。现在所有的辅助方法都准备完毕,可以开始实现TCP Server了。

Native TCP Server方法

`natvieStartTcpServer`方法是TCP Echo应用的核心。在`Echo.cpp`中增加`nativeStartTcpServer`方法:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativeexe_echo_EchoServerActivity_nativeStartTcpServer(JNIEnv *env, jobject thiz,
                                                                        jint port) {

    //创建一个新的Tcp server
    int serverSocket = NewTcpSocket(env, thiz);
    if (nullptr == env->ExceptionOccurred()) {
        //绑定socket到端口
        BindSocketToPort(env, thiz, serverSocket, port);
        if (nullptr != env->ExceptionOccurred()) {
            goto exit;
        }

        //如果是随机端口
        if (0 == port) {
            //获取当前绑定的端口
            GetSocketPort(env, thiz, serverSocket);
            if (nullptr != env->ExceptionOccurred()) {
                goto exit;
            }
        }

        //监听socket,backlog是4
        ListenOnSocket(env, thiz, serverSocket, 4);
        if (nullptr != env->ExceptionOccurred()) {
            goto exit;
        }

        //接受一个客户端socket连接
        int clientSocket = AcceptOnSocket(env, thiz, serverSocket);
        if (nullptr != env->ExceptionOccurred()) {
            goto exit;
        }

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

        //接受并返回数据
        while (true) {
            //从socket中接收数据
            recvSize = ReceiveFromSocket(env, thiz, clientSocket, buffer, MAX_BUFFER_SIZE);

            if (0 == recvSize || nullptr != env->ExceptionOccurred()) {
                break;
            }

            //向Socket返回数据
            sentSize = SendToSocket(env, thiz, clientSocket, buffer, recvSize);
            if (0 == sentSize || nullptr != env->ExceptionOccurred()) {
                break;
            }
        }

        //关闭客户端socket连接
        close(clientSocket);
    }

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

代码逻辑非常简单,在指定端口建立一个服务端Socket然后等待连接。当连接请求到达的时候,接受这个请求,并将客户端发来的数据发送回去。

Client端

Echo Client Activity Layout

```xml

<TextView
  android:id="@id/tv_log"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  tools:text="xxxxxxx" />

</androidx.constraintlayout.widget.ConstraintLayout>


<h3 id="MbjY8">Echo Client Activity</h3>
```kotlin
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
import kotlin.jvm.Throws

class EchoClientActivity : 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 {
                nativeStartTcpClient(ip, port, message)
            } catch (e: Exception) {
                logMessage(e.message.toString())
            }
            logMessage("Client terminated.")

        }

    }

    @Throws(Exception::class)
    external fun nativeStartTcpClient(ip: String, port: Int, message: String)
}

实现Native TCP Client

接下来需要实现Native TCP 客户端。
连接到地址:connect
可以通过`connect` 方法和通过协议地址将Socket连接到Server Socket上:
int connect(int socketDescriptor, const struct sockaddr *address, 
            socklen_t addressLength);

这个方法有三个入参:

  1. socket descriptor: 表示连接到对应地址的socket实例
  2. address: socket想要连接的地址
  3. address length: address的长度

如果尝试连接成功,返回0,否则返回-1和设置errno全局变量来表示具体的错误。在Echo.cpp 中增加ConnectToAddress 方法:

static void
ConnectToAddress(JNIEnv *env, jobject obj, int sd, const char *ip, unsigned short port) {
    LogMessage(env, obj, "Connecting to %s:%uh", ip, port);
    struct sockaddr_in address;
    memset(&address, 0, sizeof(address));
    address.sin_family = PF_INET;

    //将string类型的IP转化为网络地址
    if (0 == inet_aton(ip, &(address.sin_addr))) {
        ThrowErrnoException(env, "java/io/IOException", errno);
    } else {
        //将端口转化为网络字节顺序
        address.sin_port = htons(port);
        //连接到地址
        if (-1 == connect(sd, (const sockaddr *) &address, sizeof(address))) {
            ThrowErrnoException(env, "java/io/IOException", errno);
        } else {
            LogMessage(env, obj, "Connected.");
        }
    }
}

连接成功后,可以使用POSIX Socket API方法在Client和Server之间交换数据。

Native TCP Client方法

`nativeStartTcpClient`方法是客户端发起连接的方法:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativeexe_echo_EchoClientActivity_nativeStartTcpClient(JNIEnv *env,
                                                                        jobject thiz,
                                                                        jstring ip, jint port,
                                                                        jstring message) {
    //创建一个新的TCP socket
    int clientSocket = NewTcpSocket(env, thiz);
    if (nullptr == env->ExceptionOccurred()) {
        //将IP地址转化为C string
        const char *ipAddress = env->GetStringUTFChars(ip, nullptr);
        if (nullptr == ipAddress) {
            goto exit;
        }

        //连接IP地址和端口
        ConnectToAddress(env, thiz, clientSocket, ipAddress, (unsigned short) port);

        //释放IP地址
        env->ReleaseStringUTFChars(ip, ipAddress);

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

        //将message转化为C string
        const char *messageText = env->GetStringUTFChars(message, nullptr);
        if (nullptr == messageText) {
            goto exit;
        }

        //获取message的size
        jsize messageLength = env->GetStringUTFLength(message);

        //向socket发送message
        SendToSocket(env, thiz, clientSocket, messageText, messageLength);
        //释放 message text
        env->ReleaseStringUTFChars(message, messageText);

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

        char buffer[MAX_BUFFER_SIZE];
        //从Socket接收数据
        ReceiveFromSocket(env, thiz, clientSocket, buffer, MAX_BUFFER_SIZE);
    }

    exit:
    if (clientSocket > -1) {
        close(clientSocket);
    }
}

逻辑非常简单,通过Socket连接到对应的Server,发送信息,然后显示Server返回的信息。

运行TCP Socket示例

为了演示TCP Echo应用,需要两个模拟器。

配置Echo TCP Server

Server端配置流程如下:
  1. 将端口号设置为0,在bind的时候将会分配一个随机的端口号
  2. 点击Start Server,开启Echo TCP Server

连接两个模拟器

由于Echo TCP Client和Server运行在两个模拟器中,他们之间无法建立直接的连接。Android模拟器在虚拟网络中作为一个虚拟设备运行在沙盒环境中。模拟器中的应用只能和模拟器进程所在的宿主设备通信。为了使Server、Client能够通信,端口号需要通过宿主设备进行桥接。通过ADB的端口转发方法可以实现这个功能。

打开terminal输入如下指令:

adb –s <emulator-name> forward tcp:<emulator server port number> tcp:<host forward port number>

这个命令会将模拟器的端口映射到宿主设备的端口。任何连接到宿主设备端口的连接都会被adb转发到模拟器对应的端口上。端口转发只在模拟器运行时生效,模拟器停止后就清除转发配置。

配置Echo TCP Client

步骤如下:
  1. 设置IP地址为10.0.2.2。这个静态IP用于Android模拟器和宿主设备通信
  2. 将端口号设置为之前转发的端口号
  3. 在Message输入框中随便输入一段字符串
  4. 点击Start Client开启Echo TCP Client

面向连接的协议如TCP对需要可靠连接的应用提供了保证不发生错误的通信中介。这个保证是以维护打开的连接为代价的。部分应用如多媒体类型的应用并不需要这样的可靠连接。POSIX Socket API也提供了面向无连接的通信。

总结

本章主要介绍了通过Bionic library中面向连接的POSIX Socket API,并在Server和Client端使用TCP协议进行了通信。接下来一章将会介绍通过POSIX Socket API进行面向无连接的通信。