说起网络编程许多人都觉现在开源市场上那么多框架,只要学会其中一个,其余的都基本相似。但是这么多框架你知道他们底层都是通过什么来实现的吗?假如让你实现一个私有的协议可以用哪些东西来实现呢?这就需要我们今天说的内容---------套接字socet。socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用来实现进程在网络中通信。可以这么说,Android有一些网络数据和一些与内核做数据沟通都是通过socket这么一套机制来实现的,比如DHCP过程、ARP过程、MDNS的过程以及NetLink等和内核交换数据的过程,这些都是通过socket来实现的。在使用socket的过程中,有非常多的参数和配置,这常常需要我们自己去查明一些作用,比如负载均衡、socket的复用、指定协议类型、过滤某些广播报文等。下面就我工作过程中遇到的一些socket相关做一些知识点分享。看完本篇文章你大致可以了解如下知识点:socket种类类型、socket协议族类型、协议类型、socket选项级别配置、java创建socket的基本使用流程和Android设备本地socket。
1、socket套接字的类型
学习网络编程的目的是为了开发基于互联网通信的软件,不论是BS架构的还是CS架构的。我们开发互联网通信软件是处于TCP/IP五层协议中的应用层,当涉及到数据需要经过互联网传输时,就需要使用到socket抽象层。这个socket抽象层不属于TCP/IP五层,是一个抽象的出来的,帮我们封装了包括传输层以下的其他各层。它把复杂的TCP/IP协议族隐藏在socket接口后面,对用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议。在开发时,只需要遵循socket的规定编写代码,写出来的程序自然遵循TCP/UDP协议。
JAVA 中支持套接字有八种类型之多分别是 如下:
| 类型 | 表示意义 | 常用场景 |
|---|---|---|
| SOCK_STREAM | 流式的套接字 | NetLinker |
| SOCK_DGRAM | 数据报套接字 | DHCP/ARP |
| SOCK_RAW | 原始套接字 | ICMP/ICMP |
| SOCK_RDM | 可靠传输套接字(UDP) | 进程间通信 |
| SOCK_SEQPACKET | 顺序数据包套接字 | 和SOCK_STREAM类似 |
| SOCK_NONBLOCK | 严格来说不算 结合其他套接字一起使用 | 辅助类型socket不能单独使用 |
| SOCK_CLOEXEC | 严格来说不算 结合其他套接字一起 | 辅助类型socket不能单独使用 |
| SOCK_DCCP | 一种协议类型套接字 阻塞控制流套接字 | 类似TCP/UDP协议单独使用 |
| SOCK_PACKET | 数据链路套接字 | 被遗弃了 用不到了 |
常用的是如下三种类型的协议,我们可以初步参考一下:
(1)流式套接字(SOCK_STREAM)
流式的套接字可以提供可靠的、面向连接的通讯流。如果你通过流式套接字发送了顺序的数据:"1""2",那么数据到达远程时候的顺序也是"1""2"。大名鼎鼎的telnet就是使用这个类型的报文实现的.
(2)数据报套接字(SOCK_DGRAM)
数据报套接字定义了一种无连接的服务。数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。如果你发送了一个数据报,它可能不会到达,可能会以不同的顺序到达。如果它到达了,它包含的数据中可能存在错误。 数据报套接字也使用IP,但是它不使用TCP,它使用使用者数据报协议UDP(User Datagram Protocol可以参考RFC 768)。为什么说它们是"无连接"的呢?因为它(UDP)不像流式套接字那样维护一个打开的连接。你只需要把数据打成一个包,把远程的IP贴上去,然后把这个包发送出去。这个过程是不需要建立连接的。UDP的应用例子有:tftp、bootp等。那么数据包既然会丢失,怎样能保证程序能够正常工作呢?事实上,每个使用UDP的程序都要有自己的对数据进行确认的协议。比如,TFTP协议定义了对于每一个发送出去的数据包,远程在接收到之后都要回送一个数据包告诉本地程序:"我已经拿到了!(一个"ACK"包)。如果数据包发的送者在5秒内没有的得到回应,它就会重新发送这个数据包直到数据包接受者回送了"ACK"信号。这些知识对编写一个使用UDP协议的程序员来说是非常必要的。无连接服务器一般都是面向事务处理的,一个请求一个应答就完成了客户程序与服务程序之间的相互作用。面向连接服务器处理的请求往往比较复杂没,不是一来一去的请求应答所能解决的,而且往往是并发服务器。套接字工作过程如下:服务器首先启动,通过调用socket()建立一个套接字,然后调用bind()将该套接字和本地网络地址联系在一起,再调用listen()使套接字做好监听的准备,并规定它的请求队列的长度,之后就调用accept()来接收连接。客户在建立套接字后就可调用connect()和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用read()和write()来发送和接收数据。最后,待数据传送结束后,双方调用close()关闭套接字。
(3)原始套接字(SOCK_RAW)
原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。我们常用ICMP报文就使用这样的类型socket.
2、socket协议族类型
协议族指的是socket使用的协议类型,常用的协议族有:AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,在内核中每一种协议族用一个struct net_proto_family对象与之对应,各种协议族实现各自的create接口,用以创建不同的socket。常见的协议族类型又参考如下表:
| 类型 | 名称 | 常用地方 |
|---|---|---|
| AF_INET | IPV4协议 | TCP/UDP连接 |
| AF_INET6 | IPV6协议 | TCP/UDP连接基于IPV6地址 |
| AF_NETLINK | Netlink族 | 主要用于和Linux内核通信的socket |
| AF_PACKET | 链路族 | 可以直接从链路层接收和发送报文 |
| AF_UNIX/AF_LOCAL | UNIX文件系统 | 跨进程间通讯相当于本地socket |
| AF_UNSPEC | 未指定ip协议类型 | 辅助性协议族 当不知道本地是否存在v4或者v6 |
3、协议类型
我们来看一下创建一个socket需要哪些参数:
java
// BlockGuardOs.java
public FileDescriptor socket(int domain, int type, int protocol) throws ErrnoException {
final FileDescriptor fd = super.socket(domain, type, protocol);
if (isInetDomain(domain)) {
tagSocket(fd);
}
return fd;
}
// ForwardingOs.java
// os 对象 对应着 Linux.java
public FileDescriptor socket(int domain, int type, int protocol) throws ErrnoException {
return os.socket(domain, type, protocol);
}
// 在Linux 中 对应着 native 方法
public native FileDescriptor socket(int domain, int type, int protocol) throws ErrnoException;
scss
// libcore_io_Linux.cpp
static jobject Linux_socket(JNIEnv* env, jobject, jint domain, jint type, jint protocol) {
if (domain == AF_PACKET) {
protocol = htons(protocol); // Packet sockets specify the protocol in host byte order.
}
int fd = throwIfMinusOne(env, "socket", TEMP_FAILURE_RETRY(socket(domain, type, protocol)));
return fd != -1 ? jniCreateFileDescriptor(env, fd) : NULL;
}
从整个调用链来看,传过来的参数就是三个,分别是domian、type和protocol。通常协议族和类型定好了以后,这个protocol一般都是0,因为到了底层会根据前面两个参数选择相关协议比如DHCP。过程中创建socket如下:
ini
@Override
protected FileDescriptor createFd() {
try {
mPacketSock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0 /* protocol */);
NetworkStackUtils.attachDhcpFilter(mPacketSock);
final SocketAddress addr = makePacketSocketAddress(ETH_P_IP, mIface.index);
Os.bind(mPacketSock, addr);
} catch (SocketException | ErrnoException e) {
logError("Error creating packet socket", e);
closeFd(mPacketSock);
mPacketSock = null;
return null;
}
return mPacketSock;
}
当然也可以自己强行指定协议:
ini
private boolean initUdpSocket() {
final int oldTag = TrafficStats.getAndSetThreadStatsTag(
TrafficStatsConstants.TAG_SYSTEM_DHCP);
try {
mUdpSock = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
SocketUtils.bindSocketToInterface(mUdpSock, mIfaceName);
Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_REUSEADDR, 1);
Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_BROADCAST, 1);
Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_RCVBUF, 0);
Os.bind(mUdpSock, IPV4_ADDR_ANY, DhcpPacket.DHCP_CLIENT);
} catch (SocketException | ErrnoException e) {
Log.e(TAG, "Error creating UDP socket", e);
return false;
} finally {
TrafficStats.setThreadStatsTag(oldTag);
}
return true;
}
由于协议类型过多,通常也是一些标准的协议类型,需要在使用的过程中自己选择。常见类型如下:
ini
enum {
IPPROTO_IP = 0,
#define IPPROTO_IP IPPROTO_IP
IPPROTO_ICMP = 1,
#define IPPROTO_ICMP IPPROTO_ICMP
IPPROTO_IGMP = 2,
#define IPPROTO_IGMP IPPROTO_IGMP
IPPROTO_IPIP = 4,
#define IPPROTO_IPIP IPPROTO_IPIP
IPPROTO_TCP = 6,
#define IPPROTO_TCP IPPROTO_TCP
IPPROTO_EGP = 8,
#define IPPROTO_EGP IPPROTO_EGP
IPPROTO_PUP = 12,
#define IPPROTO_PUP IPPROTO_PUP
IPPROTO_UDP = 17,
#define IPPROTO_UDP IPPROTO_UDP
IPPROTO_IDP = 22,
#define IPPROTO_IDP IPPROTO_IDP
IPPROTO_TP = 29,
#define IPPROTO_TP IPPROTO_TP
IPPROTO_DCCP = 33,
#define IPPROTO_DCCP IPPROTO_DCCP
IPPROTO_IPV6 = 41,
#define IPPROTO_IPV6 IPPROTO_IPV6
IPPROTO_RSVP = 46,
#define IPPROTO_RSVP IPPROTO_RSVP
IPPROTO_GRE = 47,
#define IPPROTO_GRE IPPROTO_GRE
IPPROTO_ESP = 50,
#define IPPROTO_ESP IPPROTO_ESP
IPPROTO_AH = 51,
#define IPPROTO_AH IPPROTO_AH
IPPROTO_MTP = 92,
#define IPPROTO_MTP IPPROTO_MTP
IPPROTO_BEETPH = 94,
#define IPPROTO_BEETPH IPPROTO_BEETPH
IPPROTO_ENCAP = 98,
#define IPPROTO_ENCAP IPPROTO_ENCAP
IPPROTO_PIM = 103,
#define IPPROTO_PIM IPPROTO_PIM
IPPROTO_COMP = 108,
#define IPPROTO_COMP IPPROTO_COMP
IPPROTO_SCTP = 132,
#define IPPROTO_SCTP IPPROTO_SCTP
IPPROTO_UDPLITE = 136,
#define IPPROTO_UDPLITE IPPROTO_UDPLITE
IPPROTO_MPLS = 137,
#define IPPROTO_MPLS IPPROTO_MPLS
IPPROTO_RAW = 255,
#define IPPROTO_RAW IPPROTO_RAW
IPPROTO_MAX
};
4、socket 选项级别配置
在使用socket时候配置一些选项,比如设置心跳包、设置报文过滤器、地址复用功能、路由寻址功能、启用广播、设置收发报文缓冲区大小、指定出局网卡等,通常这些都是通过如下方法配置:
arduino
/** 参数:
socket:文件描述符
level:协议层次
SOL_SOCKET 套接字层次
IPPROTO_IP ip层次
IPPROTO_TCP TCP层次
option_name:选项的名称(套接字层次)
SO_BROADCAST 是否允许发送广播信息
SO_REUSEADDR 是否允许重复使用本地地址
SO_SNDBUF 获取发送缓冲区长度
SO_RCVBUF 获取接收缓冲区长度
SO_RCVTIMEO 获取接收超时时间
SO_SNDTIMEO 获取发送超时时间
option_value:获取到的选项的值
option_len:value的长度
返回值:
成功:0
失败:-1*/
int setsockopt( int socket, int level, int option_name,const void *option_value, size_t option_len);
// 这边这个 level 支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6 等设置 通常我们配置Socket属性只会用到 level 为 SOL_SOCKET这个级别的 后面三个 就是协议上的配置了 不通协议支持不同配置
相关常用配置如下表格:
| 名称 | 功能说明 | 设置值类型 |
|---|---|---|
| SO_BINDTODEVICE | 绑定出局网卡(这个功能很重要) | char* |
| SO_BROADCAST | 本选项开启或禁止进程发送广播消息的能力 | int |
| SO_DEBUG | 仅仅支持TCP 套接字 开启调试 | int |
| SO_DOMAIN | 给Socket设置协议族 | |
| SO_DONTROUTE | 打开或关闭路由查找功能 | int |
| SO_ERROR | 进程然后可以通过获取SO_ERROR套接口选项来得到so_error的值。由getsockopt返回的整数值就是此套接口的待处理错误。so_error随后由内核复位为0 | int |
| SO_KEEPALIVE | TCP下的保活心跳包 | int |
| SO_LINGER | close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回 | struct linger |
| SO_OOBINLINE | 此选项打开时,带外数据将被保留在正常的输入队列中(即在线存放)。当发生这种情况时,接收函数的MSG_OOB标志不能用来读带外数据 | int |
| SO_PASSCRED | 允许或禁止SCM_CREDENTIALS 控制消息的接收 | struct ucred |
| SO_PEERCRED | 针对AF_UNIX类型 发送用户凭据(pid uid gid) | struct ucred |
| SO_PROTOCOL | 配置协议 | |
| SO_RCVBUF | 设置接受缓冲区大小 | int |
| SO_RCVLOWAT | 设置接受可读报文的最低大小 | int |
| SO_RCVTIMEO | 接受报文超时值 | struct timeval |
| SO_REUSEADDR | 打开或关闭地址复用功能 | int |
| SO_REUSEPORT | 端口复用 允许同一个服务在同一个端口下建立多个 | int |
| SO_SNDBUF | 设置发送缓冲区的大小 | int |
| SO_SNDLOWAT | 设置发送缓冲区最小可用空间 | int |
| SO_SNDTIMEO | 设置发送报文超时值 | struct timeval |
| SO_TYPE | 设置Socket类型 | |
| SO_ATTACH_FILTER | 设置过滤报文功能(非常重要) | struct sock_filter |
因为设备接入以太网存在非常多的报文经过,我们socket监听如果没有适当过滤掉某些报文能力,会影响到我们的应用程序的功能,比如在设备获取ip地址过程中我们就很有必要去设置一下提取bootstap协议报文。如果不设置过滤器 ,广播类的报文太多导致dhcp客户端会不停处理报文,很难在正确报文过来的时候得到正确的处理。在Android设备DhcpClient中就设置了一个过滤器:
ini
NetworkStackUtils.attachDhcpFilter(mPacketSock);
scss
static void network_stack_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd) {
static sock_filter filter_code[] = {
// Check the protocol is UDP.
BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kIPv4Protocol),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_UDP, 0, 6),
// Check this is not a fragment.
BPF_STMT(BPF_LD | BPF_H | BPF_ABS, kIPv4FlagsOffset),
BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, IP_OFFMASK, 4, 0),
// Get the IP header length.
BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, kEtherHeaderLen),
// Check the destination port.
BPF_STMT(BPF_LD | BPF_H | BPF_IND, kUDPDstPortIndirectOffset),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, kDhcpClientPort, 0, 1),
// Accept or reject.
BPF_STMT(BPF_RET | BPF_K, 0xffff),
BPF_STMT(BPF_RET | BPF_K, 0)
};
static const sock_fprog filter = {
sizeof(filter_code) / sizeof(filter_code[0]),
filter_code,
};
int fd = jniGetFDFromFileDescriptor(env, javaFd);
if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
jniThrowExceptionFmt(env, "java/net/SocketException",
"setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
}
}
过滤器的配置比较复杂,这边粗略介绍一下如何配置,至于详细功能具体报文,需要详细了解BPF_STMT和BPF_JUMP 相关知识,还需要了解协助报文结构相关知识。这边仅仅举一个例子,说明这个功能的重要性。
5、JAVA创建Socket的基本使用流程
socket的使用还是比较简单,无非就是几个参数确认。先确认自己需要什么类型协议族,什么类型的socket可以满足数据结构,然后确认自己是服务端还是客户端,服务端就多了bind listen accept等操作;客户端只需要创建socket,然后执行connect即可。整体流程如下图所示:

Android设备本地Socket
在Android设备中很多服务都会基于init.rc的方式启动。在这个配置文件中会配置很多本地类型的socket,也就是AF_UNIX或者PF_UNIX类型的socket。这样的socket通常不会跨设备调用,都是基于跨本设备进程通信的。因为Android中还是存在很多底层进程运行来维持功能正常,比如常见的安装apk、网络服务netd、设备文件管理系统vold等功能。在这些进程开机运行起来就会创建一些LocalSocket,如netd配置文件中:
perl
service netd /system/bin/netd
class main
capabilities CHOWN DAC_OVERRIDE DAC_READ_SEARCH FOWNER IPC_LOCK KILL NET_ADMIN NET_BIND_SERVICE NET_RAW SETUID SETGID
socket dnsproxyd stream 0660 root inet
socket mdns stream 0660 root system
socket fwmarkd stream 0660 root inet # 创建sokcet 名称为fwmarkd 流式报文 权限配置 属于用户root inet 权限
onrestart restart zygote
onrestart restart zygote_secondary
# b/121354779: netd itself is not updatable, but on startup it dlopen()s the resolver library
# from the DNS resolver APEX. Mark it as updatable so init won't start it until all APEX
# packages are ready.
updatable
在这个配置中我们看到创建了三个socket,名称分别为dnsproxyd、mdns、fwmarkd,功能分别不一样。名称分别为dnsproxyd用于dns代理,dns提供局域网内的本地服务添加、移除、发现、解析等能力,fwmarkd用于iptables 功能。上层服务通过创建socket的客户端就可以向netd进程发送相关命令或者发送数据.
5、localsocket
localsocket是基于Unix域的套接字,通过fd在内核空间中进行读写操作,通常用于android zygote fork进程,因为其单线程,线程安全特性。 具体可参考:进程间通信(IPC):LocalSocket
5.1 通信流程
服务端流程:
(1)调用 socket(AF_UNIX, SOCK_STREAM, 0) 创建流式套接字。
(2)通过 bind 将套接字绑定到指定路径或命名空间。
(3)调用 listen 设置最大连接队列。
(4)通过 send 和 recv 读写数据。
客户端流程:
(1)创建套接字并连接到服务器地址(connect)。
(2)使用 send 和 recv 与服务器交换数据。
5.2 通信原理
内核提供localsocket缓存区,通过系统调用(send/recv)读写数据,可以传递文件描述符(如打开的文件、设备或套接字)。流式模式(SOCK_STREAM)保证数据有序、可靠;数据报模式(SOCK_DGRAM)则提供无连接的快速传输。
- 高效性:数据传输在内核空间完成,无用户态到内核态的多次拷贝,性能接近共享内存。
- 连接管理:流式套接字提供可靠的面向连接通信,内核负责连接建立和断开。
- 扩展性:支持传递复杂数据结构(如文件描述符)。
总结
要想全部掌握aocket相关功能是比较复杂的,一般我们了解aocket具有哪些配置功能,然后针对这些功能有所目的去搜索这些知识就行了,因为socket知识是非常有规律性质的。作为java开发人员,因为jdk中提供支持非常有限制,很多socket属性不能够在java中配置,需要我们通过jni的方式去处理一些属性配置,如报文过滤器等功能。了解socket的使用流程,对于学习一些网络请求框架非常有效果,这样能知晓里面的实现原理。 在Android中的一些跨进程实现除了常见的Binder,还可以使用socket。但是Binder更加轻量化,socket就显得非常笨重,通常都是基于内核间通信才会用到socket。