转载请注明出处:http://blog.csdn.net/andywant2fly/article/details/44154429#
众所周知,Java层已经提供一个简单易用的Socket,那为什么还要舍近求远,自己重新封装一个Native层的Socket呢?相较于Java层的Socket,Native层的Socket可以选择的配置项更多,并且能够比较准确的获取当前的拥塞状态等信息,我们可以针对不同的网络环境和应用场景配置合理的参数以达到最优的体验。当然这是一个选择的问题,如果觉得Java层的Socket足以满足需要,也不一定非要使用Native Socket不可。
那么Native Socket有哪些配置项呢?由于移动网络高RTT和高丢包的特性,移动网络的通信多基于TCP协议,所以我们这里主要讨论TCP协议。在NDK相关平台的include文件夹下的socket.h和tcp.h中列出可以配置的项(这里以Android4.0为例):
socket.h
#define SO_DEBUG 1
#define SO_REUSEADDR 2
#define SO_TYPE 3
#define SO_ERROR 4
#define SO_DONTROUTE 5
#define SO_BROADCAST 6
#define SO_SNDBUF 7
#define SO_RCVBUF 8
#define SO_SNDBUFFORCE 32
#define SO_RCVBUFFORCE 33
#define SO_KEEPALIVE 9
#define SO_OOBINLINE 10
#define SO_NO_CHECK 11
#define SO_PRIORITY 12
#define SO_LINGER 13
#define SO_BSDCOMPAT 14
#define SO_PASSCRED 16
#define SO_PEERCRED 17
#define SO_RCVLOWAT 18
#define SO_SNDLOWAT 19
#define SO_RCVTIMEO 20
#define SO_SNDTIMEO 21
#define SO_SECURITY_AUTHENTICATION 22
#define SO_SECURITY_ENCRYPTION_TRANSPORT 23
#define SO_SECURITY_ENCRYPTION_NETWORK 24
#define SO_BINDTODEVICE 25
#define SO_ATTACH_FILTER 26
#define SO_DETACH_FILTER 27
#define SO_PEERNAME 28
#define SO_TIMESTAMP 29
#define SCM_TIMESTAMP SO_TIMESTAMP
#define SO_ACCEPTCONN 30
#define SO_PEERSEC 31
#define SO_PASSSEC 34
#define SO_TIMESTAMPNS 35
#define SCM_TIMESTAMPNS SO_TIMESTAMPNS
#define SO_MARK 36
#define SO_TIMESTAMPING 37
#define SCM_TIMESTAMPING SO_TIMESTAMPING
#define SO_PROTOCOL 38
#define SO_DOMAIN 39
#define SO_RXQ_OVFL 40
#define SO_WIFI_STATUS 41
#define SCM_WIFI_STATUS SO_WIFI_STATUS
tcp.h
#define TCP_NODELAY 1
#define TCP_MAXSEG 2
#define TCP_CORK 3
#define TCP_KEEPIDLE 4
#define TCP_KEEPINTVL 5
#define TCP_KEEPCNT 6
#define TCP_SYNCNT 7
#define TCP_LINGER2 8
#define TCP_DEFER_ACCEPT 9
#define TCP_WINDOW_CLAMP 10
#define TCP_INFO 11
#define TCP_QUICKACK 12
#define TCP_CONGESTION 13
这么多配置项是否有种眼花缭乱的感觉,感兴趣的朋友可以去网上搜一搜每个配置项的具体作用。这里列举几个比较重要的配置项:
TCP_MAXSEG,首先介绍一下MTU和MSS,这两个数据在TCP协议中是重要的概念。MTU,Maxitum Transmission Unit ,表示一个接口无需分片所能发送的数据包最大字节数;MSS,Maxitum Segment Size 表示一个接口无需分片每次能够传输的最大数据分段,TCP_MAXSEG对应的是MSS的值,MSS = MTU - IP层和其之上的所有协议头的大小。如果是如果是TCP协议的话,MTU为1500,减去TCP和IP报头,MSS为1460。那MTU和MSS在数据传输中扮演什么样的角色呢?如果数据报文大于MTU,并且在IP层设置了允许分片,则会将数据包以MTU值为依据分片发送出去,每次分片都会增加一个IP报头和一个二层帧头和帧尾,由于IP层不支持重传机制,若其中有一片丢失会导致整个数据包发送失败并在TCP层重传,如果IP层不允许分片则会导致发送失败。分片会降低传输效率,尤其在移动网络高RTT和高丢包的情况下,表现更明显,因此MTU是一个重要的参考值。通常情况我们更关注每次能够发送的最大数据量,而不关注各部分协议头的大小,因此我们MSS才是更适合我们的。一般情况下TCP_MAXSEG我们不需要去设置,TCP协议在三次握手的过程中可以协商获取MSS值。
SO_SNDBUF和SO_RCVBUF,顾名思义,配置Socket发送和接受的缓冲区大小,设置成多少合适呢?我个人认为可以取大于Socket单位时间内最大吞吐量的值*2为宜。单位时间内最大吞吐量:MSS/RTT*1s,RTT表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延。过小的话在网络连接高速且稳定的时候浪费带宽。其中在客户端SO_RCVBUF应在connect之前设置。
SO_KEEPALIVE、TCP_KEEPIDLE、TCP_KEEPINTVL和TCP_KEEPCNT,注意,第一个参数是socket层的配置项,配置是否打开socket心跳,后面三个则是TCP层的配置项.TCP_KEEPIDLE表示心跳机制启动前TCP空闲时间,默认为7200s,TCP_KEEPINTVL表示两次未确认心跳之间的间隔,默认为75s,TCP_KEEPCNT表示断开前重试次数默认为9次。由此看来,默认值明显不合理。我们知道短连接中,从APP请求到服务器返回,需要经历TCP三次握手、发送请求和获取回复报文,在报文较小的情况下,一次网络通信,三次握手占据了一半以上的时间,所以为了达到更快的响应速度,我们倾向于在APP活跃期间,在APP和服务器之间保持长连接,以减少多余的TCP握手请求。此时就需要使用心跳机制来保持长连接或确认保持的连接是否可用。此外由于运营商会定时从路由表上踢掉不活跃的链接,所以我们也可以利用keepalive避免被运营商干掉链接。
SO_RCVTIMEO和SO_SNDTIMEO,相信大家可以根据名字一眼就看出来,这是设置发送和接受超时的
TCP_NODELAY和TCP_CORK,TCP默认启用Nagel算法来解决糊涂窗口综合症,这两个配置项即和此相关。由于我们的移动APP对实时性的要求比较高,我们可以通过配置TCP_NODELAY参数关闭Nagel算法,此时无论多小的包都会立即发送。而TCP_CORK若配置打开,则会尽可能的进行数据组包,在达到MTU或者一定的超时后发送,一般情况下在发送较大数据的时候可以提高网络带宽的利用率。因此,当我们在传输较大文件如图片、文件的时候,可以在此期间打开TCP_CORK,结束时关闭。默认情况下TCP_CORK的优先级要高于TCP_NODELAY。
TCP_INFO,该配置项只能读取,不能设置,包含的内容很多。我们可以根据这些信息来更准确的判断网络状态和更合理的优化重传策略
struct tcp_info {
__u8 tcpi_state; /* TCP状态 */
__u8 tcpi_ca_state; /* TCP拥塞状态 */
__u8 tcpi_retransmits; /* 超时重传的次数 */
__u8 tcpi_probes; /* 持续定时器或保活定时器发送且未确认的段数*/
__u8 tcpi_backoff; /* 退避指数 */
__u8 tcpi_options; /* 时间戳选项、SACK选项、窗口扩大选项、ECN选项是否启用*/
__u8 tcpi_snd_wscale : 4, tcpi_rcv_wscale : 4; /* 发送、接收的窗口扩大因子*/
__u32 tcpi_rto; /* 超时时间,单位为微秒*/
__u32 tcpi_ato; /* 延时确认的估值,单位为微秒*/
__u32 tcpi_snd_mss; /* 本端的MSS */
__u32 tcpi_rcv_mss; /* 对端的MSS */
__u32 tcpi_unacked; /* 未确认的数据段数,或者current listen backlog */
__u32 tcpi_sacked; /* SACKed的数据段数,或者listen backlog set in listen() */
__u32 tcpi_lost; /* 丢失且未恢复的数据段数 */
__u32 tcpi_retrans; /* 重传且未确认的数据段数 */
__u32 tcpi_fackets; /* FACKed的数据段数 */
/* Times. 单位为毫秒 */
__u32 tcpi_last_data_sent; /* 最近一次发送数据包在多久之前 */
__u32 tcpi_last_ack_sent; /* 不能用。Not remembered, sorry. */
__u32 tcpi_last_data_recv; /* 最近一次接收数据包在多久之前 */
__u32 tcpi_last_ack_recv; /* 最近一次接收ACK包在多久之前 */
/* Metrics. */
__u32 tcpi_pmtu; /* 最后一次更新的路径MTU */
__u32 tcpi_rcv_ssthresh; /* current window clamp,rcv_wnd的阈值 */
__u32 tcpi_rtt; /* 平滑的RTT,单位为微秒 */
__u32 tcpi_rttvar; /* 四分之一mdev,单位为微秒v */
__u32 tcpi_snd_ssthresh; /* 慢启动阈值 */
__u32 tcpi_snd_cwnd; /* 拥塞窗口 */
__u32 tcpi_advmss; /* 本端能接受的MSS上限,在建立连接时用来通告对端 */
__u32 tcpi_reordering; /* 没有丢包时,可以重新排序的数据段数 */
__u32 tcpi_rcv_rtt; /* 作为接收端,测出的RTT值,单位为微秒*/
__u32 tcpi_rcv_space; /* 当前接收缓存的大小 */
__u32 tcpi_total_retrans; /* 本连接的总重传个数 */
};
TCP_CONGESTION,可以配置TCP的拥塞算法,linux支持多种拥塞算法,不同的网络环境下各种拥塞算法的效率也不一样,从资料上得来的结论,westwood算法可能更适合移动网络。可惜的是,在Android的/proc/sys/net/ipv4目录下通过cat tcp_allowed_congestion_control查看Android中支持的可以配置的拥塞算法只有cubic和reno两种,因此我们无法测试哪种拥塞算法更适合国内的网络环境,也无法根据网络环境配置合适的拥塞算法。
配置Native Socket的两个主要函数为:
1. int getsockopt(int s,int level,intoptname, void *optval,socklen_t *optlen);
2. int setsockopt(int s,int level,intoptname,const void *optval,socklen_toptlen);
sock:将要被设置或者获取选项的套接字。
level:选项所在的协议层,我们需要用的SOL_SOCKETSO_TCP,其他的协议标准感兴趣的朋友可以自己查找
optname:需要访问的选项名。
optval:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值的缓冲。
optlen:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),是选项的长度。
以TCP_CONGESTION为例:
jstringJNICALL Java_com_yuancong_mobilesocket_MobileSocket_getCongestionControl(
JNIEnv *env,jobject obj, jint socket_fd)
{
char name[16] = { 0 };//拥塞算法的名称一般不超过16个字节
socklen_t t =sizeof(name);
if (getsockopt(socket_fd,SOL_TCP, TCP_CONGESTION, name, &t) < 0)
{
ThrowException(env, MOBILESOCKETEXCEPTION,"Socket get option error");
}
return (*env)->NewStringUTF(env, name);
}
void JNICALL Java_com_yuancong_mobilesocket_MobileSocket_setCongestionControl(
JNIEnv *env, jobject obj, jint socket_fd, jbyteArray name)
{
jint len = (*env)->GetArrayLength(env, name);
char *congestion = (char*) malloc(sizeof(char) * len);
(*env)->GetByteArrayRegion(env, name, 0, len, congestion);
if(setsockopt(socket_fd, SOL_TCP, TCP_CONGESTION, congestion, len)<0)
{
ThrowException(env, MOBILESOCKETEXCEPTION,"Socket set option error");
}
free(congestion);//在jni向java层抛出异常后,还会继续执行后面的代码,释放动作会被执行
}
现有的网络协议大多数都缺少对移动网络的优化,我们采用Native Socket,就是希望在理解TCP协议各种算法原理的情况下,灵活配置定制合适的策略提高我们的APP在移动网络环境下的体验。
文中如果有错误或者不足之处,欢迎大家沟通交流,谢谢。