native代码运行在单独的环境中,但是需要和主应用通信或者向外部提供服务。在前面的章节中通过JNI的方式可以和Java主应用通信。在本章中,将会使用Bionic中POSIX Socket API来让native代码直接和外部通信而无需通过Java层。
Socket是一个通过名称和地址链接链接节点在应用间传输数据的连接方式。这些应用可以在同一台设备上也可以在网络中其他设备上。POSIX Socket API(之前被称为Berkeley Sokcet API),是一个可以通过各种协议传输数据高度灵活的API。
本章将会简单介绍POSIX Socket API和Android平台有关的部分:
- POSIX Socket整体介绍
- Socket 家族
- 面向连接的socket
在详细深入面向连接通信的POSIX socket API之前,需要创建一个间的名为Echo的应用。这个应用将会作为一个实验台来帮助了解socket编程的方方面面。
Echo Socket示例工程
工程会包含如下内容:- 一个配置socket必要参数的简单UI
- 一个简单的echo 服务,用于向发送者返回发送内容
- 简单的样板代码用于在native完成socket编程
- 一个面向连接的socket通信例子
- 一个无链接socket通信示例
- 一个本地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.echoimport 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);
这个方法接收三个入参:
domian表示了Socket将要发生domain 和协议族。在写本章时,Android 平台提供了如下协议族:- PF_LOCAL:主机内部通信协议族。这个协议族允许运行在同一个物理设备上的应用使用Socket API进行通信
- PF_INET:Internet version 4 协议族。这个协议族允许应用通过网络进行通信
Type指明了通信的语义。支持的类型如下:SOCKET_STREAM: Stream socket类型使用TCP协议提供了面向连接的通信SOCKET_DGRAM: Datagram socket类型使用UDP协议提供了无连接的通信
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);
这个方法有三个入参:
- socket descriptor表明将要被绑定到socket
- address表明socket将要被绑定到的协议地址
- 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() 方法做一个转换。这是因为端口在主机和网络中表示的字节顺序是不一样的。
网络字节顺序
不同的架构设备使用不同的字节顺序在硬件层表示数据 。这就是设备的字节顺序,或者端。例如:- Big-endian,大端字节顺序。按字节从高到低位存储
- Little-endian,小端字节存储。按字节从低到高位存储
不同字节顺序的设备不能够直接交换数据。为了让不同字节顺序的设备能够在网络中交换数据,网络协议规定大端字节顺序作为交换数据的规定。
因为JVM已经使用了大端字节顺序,Java应用无需做任何处理就可以直接在网络中交换数据。与之相对,native代码部分使用的是设备字节顺序。
- ARM 和x86架构使用小端字节顺序
- MIPS架构使用大端字节顺序
natvie代码在网络中交换数据的时候需要做适当的转换,将设备字节顺序转换为网络字节顺序。
socket库提供了一系列方法用于native代码转换字节顺序。这些方法通过sys/endian.h 头文件提供。
#include <sys/endian.h>
这个头文件会提供一下方法:
htons将unsigned short从宿主字节顺序转换为网络字节顺序nthos和htons相反,将unsigned short从网络字节顺序转化为宿主字节顺序htonl将unsinged integer从宿主字节顺序转换为网络字节顺序ntohl和htonl相反,将unsinged integer从网络字节顺序转换为宿主字节顺序
使用这些工具类方法非常方便,因为这些方法在编译期间根据目标设备的架构确定了转换的规则。如果宿主字节顺序和网络字节顺序不同,这个方法会做相应的转换,否则不做任何处理。本章后续将会经常使用这些方法。
监听连接:listen
通过`listen` 方法socket可以监听连接:int listen(int socketDescriptor, int backlog);
该方法有两个入参:
- socket descriptor: 指明了需要监听连接的socket实例对象
- 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方法有三个入参:
- socket descriptor:表明要处理的socket
- 指向address 结构体的指针,这个结构体里有连接客户端的协议地址。如果服务端应用不需要这个信息,可以设置为NULL
- 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状态,直到新的数据传输过来。这个方法有四个入参:
- socket descriptor:用于表明想要接收数据的socket
- 指向内存地址的buffer指针,用于从socket中接收数据
- buffer length,表明buffer的size。
recv方法只会填充满bufferLength后才会返回 - 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方法有四个入参:
- socket descriptor:表明想要向哪个socket中发送数据
- 指向内存地址的buffer指针,socket会将这个内存中的数据发送出去
- buffer length标明了buffer的size。
send方法只会发送buffer length长度的数据然后返回 - 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);
这个方法有三个入参:
- socket descriptor: 表示连接到对应地址的socket实例
- address: socket想要连接的地址
- 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端配置流程如下:- 将端口号设置为0,在bind的时候将会分配一个随机的端口号
- 点击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
步骤如下:- 设置IP地址为10.0.2.2。这个静态IP用于Android模拟器和宿主设备通信
- 将端口号设置为之前转发的端口号
- 在Message输入框中随便输入一段字符串
- 点击Start Client开启Echo TCP Client
面向连接的协议如TCP对需要可靠连接的应用提供了保证不发生错误的通信中介。这个保证是以维护打开的连接为代价的。部分应用如多媒体类型的应用并不需要这样的可靠连接。POSIX Socket API也提供了面向无连接的通信。