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_INETAF_INET6AF_LOCAL(或称AF_UNIXUnixsocket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

参数type用于指定套接字的类型,即我们上面讲到的socket的类型。

参数protocol用于指定套接字使用的通信协议。正常情况下,对于给定的协议族,只有单一的协议支持特定的套接字类型。这时,只要将protocol参数设置为0即可

创建socket时,返回的socket描述字它存在于协议族(address familyAF_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);

}

}

}

更多相关文章

  1. C语言函数以及函数的使用
  2. android传递参数
  3. Android参数传递总结
  4. ubuntu 陪在android sdk 地址映射
  5. 史上版本最全android源码下载地址
  6. android---------ndk中的各个版本的下载地址。
  7. Android 获取当前设备的IP地址
  8. android执行外部程序,类似DELPHI里的EXEC函数

随机推荐

  1. Android(安卓)使用自定义View画圆
  2. Android隐藏状态栏、导航栏
  3. Android(安卓)MVC模式你真的明白了吗??
  4. Android的水平进度条和圆形进度条实例
  5. android 控件抖动
  6. Android之Fragment界面布局实例
  7. android 两点缩放字体
  8. Android(安卓)UserManager: Check if use
  9. android 显示gif图片
  10. Android编译系统模块中的LOCAL_XXX变量