项目代码:https://github.com/VincentWei95/ndk
Android JNI开发系列:第一章 JNIEnv接口指针
Android JNI开发系列:第三章 对引用数据类型的操作
Android JNI开发系列:第八章 POSIX Socket API 面向连接的通信
Android JNI开发系列:第九章 POSIX Socket API 无连接的通信
Android JNI开发系列:第十章 POSIX Socket API 本地通信
1 实现原生TCP Server
1.1 创建一个Socket
socket用一个名为 socket
描述符的整数表示。除了创建socket的函数外,Socket API函数需要有效的socket描述符才能正常工作。可以用 socket
函数来创建socket。
// 创建socket成功会返回相关socket描述符,否则返回-1且全局变量errno被相应地设置成错误值
int socket(int domain, int type, int protocol);
参数说明:
-
int domain:指定将会产生通信的socket域,并且选择将用到的协议族。Android支持以下协议族:
-
PF_LOCAL:主机内部通信协议族,该协议族使物理上运行在同一台设备上的应用程序可以用Socket APIs彼此通信
-
PF_INET:Internet第4版协议族,该协议族使应用程序可以与网络上其他地方运行的应用程序进行通信
-
-
int type:指定通信的语义。支持以下几种主要的socket类型:
-
SOCK_STREAM:提供使用TCP协议的、面向连接的通信Stream socket类型
-
SOCK_DGRAM:提供使用UDP协议的、无连接的通信的Datagram socket类型
-
-
int protocol:指定将会用到的协议。对于大多数协议族和类型来说,只能使用一个协议。为了选择默认协议,该参数可以设为0
1.2 将socket与一个地址绑定
当用socket函数创建一个socket后,该socket存在一个socket族空间中,且没为该socket分配协议地址。为了使客户能够定位到这个socket并与之相连,它需要先与一个地址绑定,可以用 bind
函数将socket与地址绑定。
// 如果绑定成功返回0,否则返回-1且errno全局变量被设置为相应的错误值
int bind(int socketDescriptor, const struct sockaddr* address, socklen_t addressLength);
参数说明:
-
int socketDescriptor:socket描述符,指定将绑定到指定地址的socket实例
-
const struct sockaddr* address:指定socket被绑定的协议地址。不同协议族使用不同的协议地址。PF_INET协议族使用sockaddr_in_structure指定协议地址:
struct sockaddr_in {
sa_family)t sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
};
- socklen_t addressLength:指定传递给函数的协议地址结构的大小
1.3 网络字节排序
在硬件层上,不同的机器体系结构使用不同的数据排序和表示规则,者被称为机器字节排序或字节序。例如:
-
Big-endian字节顺序首先存储最重要的字节
-
Little-endian字节顺序首先存储不重要的字节
字节排序规则不同的机器不能直接交换数据。为了使字节排序规则不同的机器能在网络上通信,IP将big-endian字节排序声明为官方的数据传输网络字节排序规则。
由于java虚拟机已经在使用big-endian字节排序,java应用程序在进行跨网络通信时,不一定要做数据转换。与此相反,因为java虚拟机不执行原生组件,所以它们使用机器字节排序:
-
ARM和x86机器结构使用little-endian字节排序
-
MIPS机器结构使用big-endian字节排序
在网络上通信时,原生代码需要在机器字节排序和网络字节排序间做必要的转换。
socket库提供了一组便利函数,使原生应用程序可以透明地处理字节排序转换,函数通过 sys/endian.h
头文件声明。
#include <sys/endian.h>
头文件提供了以下便利函数:
-
htons函数:将
unsigned short
从主机字节排序转换到网络字节排序 -
ntohs函数:和
htons
函数相反,将unsigned short
从网络字节排序转换到主机字节排序 -
htonl函数:将
unsigned integer
从主机字节排序转换到网络字节排序 -
ntohl函数:和
htonl
函数相反,将unsigned integer
从网络字节排序到主机字节排序
1.4 监听进入的连接
启动给定socket上输入连接的监听。
// 如果成功返回0,否则返回-1且errno全局变量被设置为相应的错误值
int listen(int socketDescriptor, int backlog);
参数说明:
-
int socketDescriptor:指定应用程序想要监听的输入连接socket实例
-
int backlog:指定保存挂起的输入连接的队列大小。如果应用程序正在忙于为客户服务,其他输入连接就要排队,队列中挂起的连接数的最大值由
backlog
指定。当输入连接达到backlog
所限定的值时,其他的输入连接将被拒绝
1.5 接受传入连接
accept
函数用来显示地输入连接从监听队列里取出并接受它。
accept
函数是一个阻塞函数,如果在监听队列中没有即将到来的输入连接请求,它会使调用进程进入挂起状态,直到有新的输入连接。
// 如果成功返回与该连接实例交互时将会用到的客户socket描述符
// 否则返回-1且全局变量errno被设置为合适的错误值
int accept(int socketDescriptor, struct sockaddr* address, socklen_t* addressLength);
参数说明:
-
int socketDescriptor:指定应用程序想要从其他接受输入连接的socket实例
-
struct sockaddr* address:提供了一个地址结构,在该结构中填入被连接的客户协议地址。如果应用程序不需要要该信息,它可以设置为NULL
-
socklen_t* addressLength:为要填入的连接客户协议地址提供指定大小的内存空间。如果不需要该信息,它可以被设置为NULL
1.6 从socket接收数据
recv
函数也是一个阻塞函数,如果没有从给定的socket接收到数据,它会调用进程进入挂起状态,直到接收到可用数据。
// 如果成功返回从socket那里接收到的字节数,否则返回-1且全局变量errno将被设置为相应的错误值
// 如果返回0表示socket连接失败
ssize_t recv(int socketDescriptor, void* buffer, size_t bufferLength, int flags);
参数说明:
-
int socketDescriptor:指定应用程序想要从中接收数据的socket实例
-
void* buffer:指向内存地址的指针,该内存用来保存从socket接收的数据
-
size_t bufferLength:指定缓冲区大小,
recv
函数只会向缓冲区写入该参数指定大小的内容然后返回 -
int flags:指定接收所需要的额外标志
1.7 向socket发送数据
send
函数也是一个阻塞函数,如果socket在忙着发送数据,它会使调用进程进入挂起状态直到socket可以传输数据。
// 如果成功返回传送的字节数,否则返回-1且全局变量errno将被设置为相应的错误值
// 如果返回0表示socket连接失败
ssize_t send(int socketDescriptor, void* buffer, size_t bufferLength, int flags);
参数说明:
-
int socketDescriptor:指定应用程序想要向其发送数据的socket实例
-
void* buffer:指向内存地址的buffer指针,该内存是给定的socket发送数据的目的地
-
size_t bufferLength:指定缓冲区大小。
send
函数只会向缓冲区传输该参数所指定大小的数据然后返回 -
int flags:指定发送所需要的额外标志
2 实现原生TCP Client
通过提供协议地址来连接socket和server socket,由 connect
函数完成。
// 如果成功返回0,否则返回-1且全局变量errno设置为相应的错误值
int connect(int socketDescriptor, const struct sockaddr* address, socklen_t addressLength);
参数说明:
-
int socketDescriptor:指定应用程序想要连接协议地址的socket实例
-
const struct sockaddr* address:指定socket要连接的协议地址
-
socklen_t addressLength:指定所提供的地址结构的长度