socket()
为通讯创建一个端点,为套接字返回一个文件描述符
int socket(int domain, int type, int protocol);
domain 为套接字指定协议集
- AF_UNIX, AF_LOCAL
AF_UNIX=AF_LOCAL本地socket,用于本机进程间通信 - AF_INET
表示IPv4协议 - AF_INET6
表示IPv6协议 - AF_NETLINK
用于kernel和user之间的通信,比如用户空间接受内核发出的消息事件
type
- SOCK_STREAM
可靠的面向流服务或流套接字,TCP - SOCK_DGRAM
数据报文服务或者数据报文套接字 UDP - SOCK_RAW
在网络层之上自行指定运输层协议头,即原始套接字
2.6.67内核之后,type参数又添加了一个新的目的: 用于定义socket的行为 - SOCK_NONBLOCK
创建的socket默认是阻塞的,该参数改变修改socket的属性为非阻塞 - SOCK_CLOEXEC
创建子进程时,文件描述符自动被关闭,不会被clone给子进程。这样做的原因如下:有时我们创建子进程后会使用exec来代替当前的子进程,但是在这之前我们需要把子进程中无用的文件描述符都关闭掉,可以手动关闭,但是如果在复杂的大型项目中,这样做显然不现实,使用这个参数,子进程不会把父进程的文件描述符clone过来,也就省去了关闭的烦恼
protocol 实际使用的传输协议
- 0
根据domain和type选择缺省协议 - IPPROTO_TCP
传输控制协议 - IPPROTO_SCTP
流控制传输协议 - IPPROTO_UDP
用户数据包协议 - IPPROTO_DCCP
数据拥塞控制协议
bind()
为套接字分配地址,当使用socket创建套接字后,是赋予了其使用的协议,并没有分配地址。在接受其它的主机连接之前,必须先调用bind为套接字分配一个地址
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd 使用bind函数的套接字描述符
addr 指向sockaddr结构的指针
addrlen sockaddr结构的长度
- struct sockaddr 用于兼容各种协议族的地址
struct sockaddr {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
- struct sockaddr_in 网络协议
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* 端口号 一些协议都有指定的端口号 */
struct in_addr sin_addr; /* 网络地址 htonl(INADDR_ANY)表示所有的网卡,一般用于服务端 */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
常见端口号可以参考https://zh.wikipedia.org/wiki/TCP/UDP%E7%AB%AF%E5%8F%A3%E5%88%97%E8%A1%A8
struct in_addr {
unsigned long s_addr;/* 4个字节,用于存放ip地址,如 192 168 9 6,每个数字占用一个字节 */
};
- struct sockaddr_nl NETLINK协议
struct sockaddr_nl {
sa_family_t nl_family; /* Address family AF_NETLINK */
unsigned short nl_pad; /* zero */
pid_t nl_pid; /* getpid() */
u_32 nl_groups;
}nl;
netlink机制的详细介绍http://blog.csdn.net/sprintwind/article/details/44204499
listen()
int listen(int sockfd, int backlog);
socket绑定地址之后,默认是想要去连接的,liste会将socket的主动连接的默认属性改为被动连接属性。然后开始监听可能的连接请求,但是这只能在有可靠数据流保证的时候(TCP)使用,如SOCK_STREAM和SOCK_SEQPACKET
sockfd 使用socke创建的描述符
backlog 监听队列的大小,每当有一个连接请求到来,就会进入此监听队列。当一个请求被accept()接受,这个请求就会被移除对列。当队列满后,新的连接请求就会返回错误,一般发生在同时有大量请求到来,服务器处理不过来的时候。
connect()
用于TCP连接,为一个套接字设置连接,参数有文件描述符和想要连接的服务器地址。
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd 客户端的socket
addr 服务器端的地址信息
addrlen 服务器端地址信息的大小
select() && poll() && epoll()
缓冲I/O又被称为标准I/O,大多数系统的默认I/O操作都是缓冲I/O。在linux的缓冲I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓冲中,也就是说I/O中的数据会先拷贝到内核的缓冲区中,然后再从内核缓冲区拷贝到用户空间。比如从网络上接收的数据会先被缓存到内核的页缓冲区,然后再将数据从内核空间复制到用户空间。
每一次I/O操作都是内核空间和用户空间数据拷贝的操作,这种数据拷贝操作所带来的的CPU以及内存开销是非常大的。
上面3个函数都是用来实现I/O多路复用的,I/O多路复用指的是在一个线程中实现对多个套接字的处理
select()
同时监视多个描述符的read/write状态,
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds
所有文件描述符的范围,即所有文件描述符的最大值+1。可以通过遍历所有listen的fd比较大小来找到最大值
readfds
需要监视那些文件描述符是否可读
writefds
需要监视那些文件描述符是否可写
exceptfds
需要监视哪些文件描述符时候可执行
timeout
等待超时的时间
返回值
- 当监视的文件描述符的状态的满足时,返回满足条件的文件件描述符的个数
- 当没有满足条件的文件描述符,并且timeout监控时间超时,返回 0,可以理解为满足条件的文件描述符的个数为0
- 出错返回-1,同时设置errno
操作文件描述符集合fs_set的宏:
FD_SET(fd, fdsetp) /* 将fd添加到集合fdsetp */
FD_CLR(fd, fdsetp) /* 删除集合fdsetp中的fd */
FD_ISSET(fd, fdsetp) /* 查看集合fdsetp中是否存在fd */
FD_ZERO(fdsetp) /* 将fdsetp清零 */
poll()
poll与select相似,都是对多个描述符的状态进行轮询,根据描述符的状态进行处理。但是poll没有没有最大文件描述符数量的限制。poll和select的一个缺点就是:每次轮询都要讲大量文件描述的数组在内核空间和用户空间进行整体复制,而不管这些文件描述符是否就绪,它的开销随着文件描述数量的增加而线性增大
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds
pollfd结构体指定了一个被监视的文件描述符,可以通过数组struct pollfd fsd[MAX]传递多个结构体,这样可以使poll()监视多个文件描述符
struct pollfd {
int fd; /* 文件描述符 */
/* POLLIN 有普通数据可读
* POLLRDNORM 有优先数据可读
* POLLPRI 有紧急数据可读
* POLLOUT 写普通数据不会被阻塞
* POLLWRNORM 写优先数据不会被阻塞
* POLLWRBAND 写紧急数据不会被阻塞
* POLLMSGSIGPOLL 消息可用
* revents域中还可能返回下列事件
* POLLER 指定的文件描述符发生错误
* POLLHUP 指定的描述符被挂起
* POLLVAL 指定的文件描述符非法
*/
short events; /* 等待的事件 */
short revents; /* 实际发生的事件 */
}
nfds struct pollfd数组的大小
timeout 超时时间 微妙
返回值
- 当监视的文件描述符的状态的满足时,返回满足条件的文件件描述符的个数
epoll
/* 创建一个epoll句柄,size用来告诉内核这个监听的数据是多大,这个参数不同于select中的
* 第一个参数,给出最大监听的fd+1,参数size并不是限制了epoll所能监听的描述符的最大个
* 数,只是对内核初始分配内部数据结构的一个建议 */
int epoll_create(int size);
创建好epoll后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd
epoll_create()的返回值
op
操作,用三个宏来表示
- EPOLL_CTL_ADD 添加对fd事件的监听
- EPOLL_CTL_DEL 删除对fd事件的监听
- EPOLL_CTL_MOD 修改对fd事件的监听
fd
要进行操作的套接字描述符
event
告诉内核需要监听什么事件
struct epoll_event {
/* EPOLLIN 文件描述符可读
* EPOLLOUT 文件描述符可写
* EPOLLPRI 文件描述符有紧急数据可读
* EPOLLERR 文件描述符发生错误
* EPOLLHUP 文件描述符被挂断
* EPOLLET 将epoll设置为边缘触发(Edge Triggered)模式,相对于水平触发(Level Triggered)来说的
* EPOLLONESHOT 只对文件描述符进行一次监听,当监听完这次事件后,如果还要继续监听这个socket的话,需要再次添加到监听队列
*/
__unit32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
}
/* 等待epfd上的事件,最多返回maxevents个事件 */
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
epfd
epoll_create()的返回值
events
用于从内核接收得到的事件的集合
maxevents
告诉内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size
timeout
超时时间,毫秒
返回值
- 成功返回满足条件的文件描述符的个数
- 超时返回0
- 错误返回-1,并设置errno的值
epoll的两种工作模式
Level Trigger(LT)
这是epoll的缺省工作方式,同时支持block和no-block socket。在这种做法中,内核通过epoll()告诉你一个文件描述符已经就绪,然后如果你不对就绪的fd进行I/O操作,内核是会继续通知你的
Edge Trigger(ET)
这是高速工作模式,只支持no-block socket。在这种做法中,内核通过epoll()告诉你一个文件描述符已经就绪,然后它会假设你已经知道文件描述符就绪了,并且不会再为那个文件描述符做就绪通知,知道你做了某些操作导致那个文件描述符不在处于就绪状态。注意,如果一直不对这个fd进行I/O操作(从而导致它再次变成未就绪状态),内核不会发送更多的通知,也就是内核每次只为fd发送一次就绪通知,这样更高效
ET模式在很大程度减少了epoll事件被重复触发的次数,因此效率要比LT模式要高。epoll工作在ET模式时必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/写操作把处理多个文件描述符的任务饿死
select poll 机制和epoll机制比较:
相同
使用select和poll机制在进行等待时,所在线程都是被移除CPU调度队列的,不会耗费CPU资源的。当等待的事件发生时,线程被唤醒,然后进行事件的处理工作
不同
- select和poll的不同主要在与poll没有最大文件描述符的限制,其它基本都相同
- 当线程被唤醒时,poll()和select()需要轮询所有的描述符,来找到唤醒事件,复杂度为O(n)会随着监听的套接字的个数的增加而线性增加,而epoll会把那个套接字发生了怎样的I/O事件通知我们,复杂度为O(1)
accept()
当应用程序接收到来自其它主机的面对数据流的连接时,通过事件通知它(比如unix的 select()系统调用)。必须用accept()初始化连接。accept为每个连接建立新的套接字,并从监听队列中移除这个连接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockefd 监听的套接字描述符
addr 客户端地址结构体信息,一个指向sockaddr的指针
addrlen客户端地址结构体的大小