Android Socket 实现
Android Socket 实现
android是基于linux的操作系统,android中socket的实现也自然是基于linux的标准来。socket在android中的应用是非常广泛的,特别是在framework及以下层。那什么是socket?socket的类型有哪些?socket有哪些基本函数?
我们都知道本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:
- 消息传递(管道、FIFO、消息队列)
- 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
- 共享内存(匿名的和具名的)
- 远程过程调用(Solaris门和Sun RPC)
- Binder(android,其实也是一种RPC)
那么网络间的如何通信呢?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIXSystem V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。
网络间进程通信用socket,本地进程通信也可以用socket。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –>读写write/read –>关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
socket类型:
SOCK_STREAM:即TCP,提供有序、可靠、双向及基于连接的字节流。支持带外传输机制
SOCK_DGRAM:即UDP,工作在传输层,进程之间通信(IP+端口),无连接,不保证数据完整性,不保证有序性,有分包机制,无流量控制机制
SOCK_SEQPACKET:提供有序、可靠、双向基于连接的数据报通信
SOCK_RAW:提供对原始网络协议的访问
SOCK_RDM:提供可靠的数据报层,但是不保证有序性
如何创建socket并实现服务端和客户端的连接呢?
socket既然是进程间通信,那必然有服务端和客户端。
服务器端可以操作的函数及步骤:
我们的socket是在服务器端创建的,用如下函数来完成:
int socket(int domain, int type, intprotocol);
参数domain即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
参数type用于指定套接字的类型,即我们上面讲到的socket的类型。
参数protocol用于指定套接字使用的通信协议。正常情况下,对于给定的协议族,只有单一的协议支持特定的套接字类型。这时,只要将protocol参数设置为0即可
创建socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
本地套接字的绑定的是struct sockaddr_un结构。structsockaddr_un结构有两个参数:sun_family、sun_path。sun_family只能是AF_LOCAL或AF_UNIX,而sun_path是本地文件的路径。
struct sockaddr_un{
sa_family_tsun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
网络socket的绑定的这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addrsin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
ipv6对应的是:
struct sockaddr_in6 {
sa_family_t sin6_family;/* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information*/
struct in6_addrsin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4)*/
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
int listen(int sockfd, int backlog);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
int accept(int sockfd, structsockaddr *addr, socklen_t *addrlen);
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向structsockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
客户端可以操作的函数:
int connect(int sockfd, const structsockaddr *addr, socklen_t addrlen);
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接
数据传输:
Send()和recv()这两个函数用于面向连接的socket上进行数据传输。
Send()函数原型为:
int send(int sockfd, const void *msg,int len, int flags);
Sockfd是你想用来传输数据的socket描述符;msg是一个指向要发送数据的指针;Len是以字节为单位的数据的长度;flags一般情况下置为0(关于该参数的用法可参照man手册)。
Send()函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序中应该将send()的返回值与欲发送的字节数进行比较。当send()返回值与len不匹配时,应该对这种情况进行处理。
char *msg = "Hello!";
int len, bytes_sent;
……
len = strlen(msg);
bytes_sent = send(sockfd, msg,len,0);
……
recv()函数原型为:
int recv(int sockfd,void *buf,int len,unsigned int flags);
Sockfd是接受数据的socket描述符;buf 是存放接收数据的缓冲区;len是缓冲的长度。Flags也被置为0。Recv()返回实际上接收的字节数,当出现错误时,返回-1并置相应的errno值。
Sendto()和recvfrom(),用于在无连接的数据报socket方式下进行数据传输。由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址。
sendto()函数原型为:
int sendto(int sockfd, const void*msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof(struct sockaddr)。Sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。
Recvfrom()函数原型为:
int recvfrom(int sockfd,void *buf,intlen,unsigned int flags,struct sockaddr *from,int *fromlen);
from是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。fromlen常置为sizeof(struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。Recvfrom()函数返回接收到的字节数或 当出现错误时返回-1,并置相应的errno。
如果你对数据报socket调用了connect()函数时,你也可以利用send()和recv()进行数据传输,但该socket仍然是数据报socket,并且利用传输层的UDP服务。但在发送或接收数据报时,内核会自动为之加上目地和源地址信息。
#define EPOLLIN0x00000001 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
#define EPOLLPRI0x00000002 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
#define EPOLLOUT0x00000004 表示对应的文件描述符可以写;
#define EPOLLERR0x00000008 表示对应的文件描述符发生错误;
#define EPOLLHUP0x00000010 表示对应的文件描述符被挂断;
#define EPOLLRDNORM0x00000040
#define EPOLLRDBAND0x00000080
#define EPOLLWRNORM0x00000100
#define EPOLLWRBAND0x00000200
#define EPOLLMSG0x00000400
#define EPOLLET0x80000000 将EPOLL设为边缘触发(EdgeTriggered)模式,这是相对于水平触发(Level Triggered)来说的
#define EPOLL_CTL_ADD1
#define EPOLL_CTL_DEL2
#define EPOLL_CTL_MOD3
typedef union epoll_data
{
void *ptr;
int fd;
unsigned int u32;
unsigned long longu64;
} epoll_data_t;
struct epoll_event
{
unsigned intevents;
epoll_data_t data;
};
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event*event);
int epoll_wait(int epfd, struct epoll_event *events, intmax, int timeout);
如何管理触发socket?
在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。
相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
第一步:
epoll_fd = epoll_create(10);------创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
第二步:
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_cl.fd, &ev);---原型 intepoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值,
第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events;/* Epoll events */
epoll_data_t data;/* User data variable */
};
第三步:
int epoll_wait(intepfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,
这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
while (!should_exit())
{
int result;
struct epoll_eventevents[10];
result = epoll_wait(epoll_fd, events,sizeof(events)/sizeof(events[0]), save_time_left()*1000);
if (result < 0)
{
// Unhandledsignals (USR1 for example) will terminate the program right off....
}
else if (result> 0)
{
int i;
for (i = 0; i< result; i++)
{
if(events[i].events & EPOLLIN)
{
idd_sock_t*sock_p = events[i].data.ptr;
sock_p->on_receive(sock_p);
}
}
}
更多相关文章
- C语言函数以及函数的使用
- android传递参数
- Android参数传递总结
- ubuntu 陪在android sdk 地址映射
- 史上版本最全android源码下载地址
- android---------ndk中的各个版本的下载地址。
- Android 获取当前设备的IP地址
- android执行外部程序,类似DELPHI里的EXEC函数