1. socket总览
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现, socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
socket是一种更为一般的进程间通信机制,可用于不同机器之间的进程间通信。
TCP socket
UDP socket
2. socket 函数及参数详解
2.1 socket函数
int socket(int domain, int type, int protocol); //返回sockfd(描述符)
对应于普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
domain
协议域,又称为协议族(family)
协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
/* Address families. */
#define AF_UNSPEC PF_UNSPEC ->Unspecified.
#define AF_LOCAL PF_LOCAL ->Local to host (pipes and file-domain) UNIX 进程通信协议。
#define AF_UNIX PF_UNIX ->Old BSD name for PF_LOCAL.
#define AF_FILE PF_FILE ->Another non-standard name for PF_LOCAL.
#define AF_INET PF_INET ->IP protocol family. Ipv4网络协议。
#define AF_AX25 PF_AX25 -> Amateur Radio AX.25. 业余无线AX.25协议。
#define AF_IPX PF_IPX -> Novell Internet Protocol. IPX-Novell协议
#define AF_APPLETALK PF_APPLETALK -> Appletalk DDP. appletalk(DDP)协议。
#define AF_NETROM PF_NETROM ->Amateur radio NetROM.
#define AF_BRIDGE PF_BRIDGE ->Multiprotocol bridge.
#define AF_ATMPVC PF_ATMPVC -> ATM PVCs. 存取原始ATM PVCs。
#define AF_X25 PF_X25 ->Reserved for X.25 project. ITU-T X.25/ISO-8208 协议。
#define AF_INET6 PF_INET6 ->IP version 6. Ipv6 网络协议。
#define AF_ROSE PF_ROSE ->Amateur Radio X.25 PLP.
#define AF_DECnet PF_DECnet ->Reserved for DECnet project.
#define AF_NETBEUI PF_NETBEUI ->Reserved for 802.2LLC project.
#define AF_SECURITY PF_SECURITY ->Security callback pseudo AF.
#define AF_KEY PF_KEY ->PF_KEY key management API.
#define AF_NETLINK PF_NETLINK 核心用户接口装置。
#define AF_ROUTE PF_ROUTE -> Alias to emulate 4.4BSD.
#define AF_PACKET PF_PACKET ->Packet family. 初级封包接口
#define AF_ASH PF_ASH ->Ash.
#define AF_ECONET PF_ECONET ->Acorn Econet.
#define AF_ATMSVC PF_ATMSVC ->ATM SVCs.
#define AF_SNA PF_SNA ->Linux SNA Project
#define AF_IRDA PF_IRDA ->IRDA sockets.
#define AF_PPPOX PF_PPPOX ->PPPoX sockets.
#define AF_WANPIPE PF_WANPIPE ->Wanpipe API sockets.
#define AF_BLUETOOTH PF_BLUETOOTH ->Bluetooth sockets.
#define AF_MAX PF_MAX
type
指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。这个参数指定一个套接口的类型,套接口可能的类型有:SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET、SOCK_RAW等等,它们分别表明字节流、数据报、有序分组、原始套接口。这实际上是指定内核为我们提供的服务抽象,比如我们要一个字节流。需要注意的,并不是每一种协议簇都支持这里的所有的类型,所以类型与协议簇要匹配。
SOCK_STREAM = 1, /* Sequenced, reliable, connection-based byte streams. */ 提供双向连续且可信赖的数据流,即TCP。
SOCK_DGRAM = 2, /* Connectionless, unreliable datagrams of fixed maximum length. */使用不连续不可信赖的数据包连接
SOCK_RAW = 3, /* Raw protocol interface. */提供原始网络协议存取
SOCK_RDM = 4, /* Reliably-delivered messages. */提供可信赖的数据包连接
SOCK_SEQPACKET = 5, /* Sequenced, reliable, connection-based, datagrams of fixed maximum length. */提供连续可信赖的数据包连接
SOCK_PACKET = 10 /* Linux specific way of getting packets at the dev level. For writing rarp and other similar things on the user level. */提供和网络驱动程序直接通信。
protocol
故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
见usr/include/linux/in.h:
IPPROTO_IP = 0, /* Dummy protocol for TCP */
IPPROTO_ICMP = 1, /* Internet Control Message Protocol */
IPPROTO_IGMP = 2, /* Internet Group Management Protocol */
IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94) */
IPPROTO_TCP = 6, /* Transmission Control Protocol */
IPPROTO_EGP = 8, /* Exterior Gateway Protocol */
IPPROTO_PUP = 12, /* PUP protocol */
IPPROTO_UDP = 17, /* User Datagram Protocol */
IPPROTO_IDP = 22, /* XNS IDP protocol */
IPPROTO_RSVP = 46, /* RSVP protocol */
IPPROTO_GRE = 47, /* Cisco GRE tunnels (rfc 1701,1702) */
IPPROTO_IPV6 = 41, /* IPv6-in-IPv4 tunnelling */
IPPROTO_PIM = 103, /* Protocol Independent Multicast */
IPPROTO_ESP = 50, /* Encapsulation Security Payload protocol */
IPPROTO_AH = 51, /* Authentication Header protocol */
IPPROTO_COMP = 108, /* Compression Header protocol */
IPPROTO_RAW = 255, /* Raw IP packets */
IPPROTO_MAX
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
几种典型socket声明:
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//TCP 四层socket,普通socket
socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//UDP
socket(AF_INET, SOCK_RAW, IPPROTO_UDP );//第三个参数可以是UDP,TCP或者ICMP;三层socket
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP));//第三个参数可以为ETH_P_ALL ETH_P_IP ETH_P_ARP等;接收以太网帧;二层socket
2.2 bind函数
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
该调用通过传递进来的文件描述符找到对应的socket结构,把一个地址族中的特定地址赋给socket,也可以说是绑定ip端口和socket。
include <netinet/in.h>
struct sockaddr {
unsigned short sa_family; // 2 bytes address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
//sockaddr结构会因使用不同的socket domain而有不同结构定义,例如使用AF_INET domain,其socketaddr结构定义如下。
struct sockaddr_in {
short sin_family; // 2 bytes e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 bytes e.g. htons(3490)
struct in_addr sin_addr; // 4 bytes see struct in_addr, below
char sin_zero[8]; // 8 bytes zero this if you want to
};
struct in_addr {
unsigned long s_addr; // 4 bytes load with inet_pton()
};
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
2.3 listen函数
int listen(int sockfd, int backlog);
listen() 声明 sockfd 处于监听状态,表示服务器愿意接收连接,并且最多允许有 backlog 个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
2.4 accept函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
该调用创建新的socket表示新的连接。
- 如果第二三个参数表示实际连接的客户端的地址信息;如果为空,代表我们对客户的身份不感兴趣,因此置为NULL;
- 第一个参数为socket创建的监听套接字,返回的是已连接套接字,两个套接字是有区别的,而且非常重要。区别:我们所创建的监听套接字一般服务器只创建一个,并且一直存在。而内核会为每一个服务器进程的客户连接建立一个连接套接字,当服务器完成对某个给定客户的服务时,连接套接字就会被关闭。 如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。
2.4 connect函数
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
2.4 close函数
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。 注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
2.5 数据发送接收函数
ssize_t send(int s, const void *buf, size_t len, int flags);
用来将数据由指定的socket传给对方主机,可以用于TCP的socket。
参数s为已建立好连接的socket;参数buf指向欲连线的数据内容,参数len则为数据长度。
参数flags一般设0,其他数值定义如下:
MSG_OOB传送的数据以out-of-band 送出。
MSG_DONTROUTE取消路由表查询。
MSG_DONTWAIT设置为不可阻断运作。
MSG_NOSIGNAL此动作不愿被SIGPIPE 信号中断。
返回值:成功则返回实际传送出去的字符数。
ssize_t sendto(int s, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
用来将数据由指定的socket传给对方主机。参数s为已建好连接的socket,如果利用UDP协议则不需经过连接操作。
参数msg指向欲连线的数据内容,参数flags 一般设0,详细描述请参考send()。
参数to用来指定欲传送的网络地址,结构sockaddr请参考bind()。
参数tolen为sockaddr的结果长度。
返回值:成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。失败返回-1。错误原因存于errno。
ssize_t recv(int s, void *buf, size_t len, int flags);
用来接收远端主机经指定的socket传来的数据,并把数据存到由参数buf 指向的内存空间,参数len为可接收数据的最大长度,可以用于TCP的socket。
flags一般设0。其他数值定义如下:
MSG_OOB接收以out-of-band 送出的数据。
MSG_PEEK返回来的数据并不会在系统内删除,如果再调用recv()会返回相同的数据内容。
MSG_WAITALL强迫接收到len大小的数据后才能返回,除非有错误或信号产生。
MSG_NOSIGNAL此操作不愿被SIGPIPE信号中断.
返回值成功则返回接收到的字符数,失败返回-1,错误原因存于errno中。
ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
用来接收远程主机经指定的socket 传来的数据,并把数据存到由参数buf 指向的内存空间,参数len 为可接收数据的最大长度,用于UDP的socket。
参数flags 一般设0,其他数值定义请参考recv()。
参数from用来指定欲传送的网络地址,结构sockaddr 请参考bind()。
参数fromlen为sockaddr的结构长度。
返回值:成功则返回接收到的字符数,失败则返回-1,错误原因存于errno中。
3 socket示例
//server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
// socket() 打开一个网络通讯端口,如果成功的话,
// 就像 open() 一样返回一个文件描述符,
// 应用程序可以像读写文件一样用 read/write 在网络上收发数据。
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
// bind() 的作用是将参数 listenfd 和 servaddr 绑定在一起,
// 使 listenfd 这个用于网络通讯的文件描述符监听 servaddr 所描述的地址和端口号。
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
// listen() 声明 listenfd 处于监听状态,
// 并且最多允许有 20 个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
listen(listenfd, 20);
printf("Accepting connections ...\n");
while (1)
{
cliaddr_len = sizeof(cliaddr);
// 典型的服务器程序可以同时服务于多个客户端,
// 当有客户端发起连接时,服务器调用的 accept() 返回并接受这个连接,
// 如果有大量的客户端发起连接而服务器来不及处理,尚未 accept 的客户端就处于连接等待状态。
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
{
buf[i] = toupper(buf[i]);
}
write(connfd, buf, n);
close(connfd);
}
}
//client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
char *str;
if (argc != 2)
{
fputs("usage: ./client message\n", stderr);
exit(1);
}
str = argv[1];
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
// 由于客户端不需要固定的端口号,因此不必调用 bind(),客户端的端口号由内核自动分配。
// 注意,客户端不是不允许调用 bind(),只是没有必要调用 bind() 固定一个端口号,
// 服务器也不是必须调用 bind(),但如果服务器不调用 bind(),内核会自动给服务器分配监听端口,
// 每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
write(sockfd, str, strlen(str));
n = read(sockfd, buf, MAXLINE);
printf("Response from server:\n");
write(STDOUT_FILENO, buf, n);
printf("\n");
close(sockfd);
return 0;
}
4 I/O 多路复用
4.1 select函数
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
select系统调用是用来让我们的程序监视多个文件句柄的状态变化的。程序会停在select这⾥里等待,直到被监视的文件句柄有一个或多个发⽣生了状态改变。关于文件句柄,其实就是一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。
maxfdp
需要监视的最大文件描述符加1。
readfds、writefds、errorfds
分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。
struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,如下:
- FD_CLR(inr fd,fd_set* set):用来清除描述词组set中相关fd 的位。
- FD_ISSET(int fd,fd_set *set):用来测试描述词组set中相关fd 的位是否为真。
- FD_SET(int fd,fd_set*set):用来设置描述词组set中相关fd的位。
- FD_ZERO(fd_set *set);用来清除描述词组set的全部位。
timeout
struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。
等待时间,这个时间内,需要监视的描述符没有事件
发⽣生则函数返回,返回值为0。设为NULL 表示阻塞式等待,一直等到有事件就绪,函数才会返回,0表示非阻塞式等待,没有事件就立即返回,大于0表示等待的时间。
返回值
大于0表示就绪的文件描述符的个数,等于0表示timeout等待时间到了,小于0表示调用失败。
具体原理如下:
- 我们通常需要额外定义一个数组来保存需要监视的文件描述符,并将其他没有保存描述符的位置初始化为一个特定值,一般为-1,这样方便我们遍历数组,判断对应的文件描述符是否发生了相应的事件。
- 采用上述的宏操作FD_SET(int fd,fd_set*set)遍历数组将关心的文件描述符设置到对应的事件集合里。并且每次调用之前都需要遍历数组,设置文件描述符。
- 调用select函数等待所关心的文件描述符。有文件描述符上的事件就绪后select函数返回,没有事件就绪的文件描述符在文件描述符集合中对应的位置会被置为0,这就是上述第二步的原因。
- select 返回值大于0表示就绪的文件描述符的个数,0表示等待时间到了,小于0表示调用失败,因此我们可以遍历数组采用FD_ISSET(int fd,fd_set *set)判断那个文件描述符上的事件就绪,然后执行相应的操作。
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<sys/time.h>
static void Usage(const char* proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int array[4096];
static int start_up(const char* _ip,int _port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
exit(2);
}
if(listen(sock,10) < 0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return -1;
}
int listensock = start_up(argv[1],atoi(argv[2]));
int maxfd = 0;
fd_set rfds;
fd_set wfds;
array[0] = listensock;
int i = 1;
int array_size = sizeof(array)/sizeof(array[0]);
for(; i < array_size;i++)
{
array[i] = -1;
}
while(1)
{
FD_ZERO(&rfds);
FD_ZERO(&wfds);
for(i = 0;i < array_size;++i)
{
if(array[i] > 0)
{
FD_SET(array[i],&rfds);
FD_SET(array[i],&wfds);
if(array[i] > maxfd)
{
maxfd = array[i];
}
}
}
switch(select(maxfd + 1,&rfds,&wfds,NULL,NULL))
{
case 0:
{
printf("timeout\n");
break;
}
case -1:
{
perror("select");
break;
}
default:
{
int j = 0;
for(; j < array_size; ++j)
{
if(j == 0 && FD_ISSET(array[j],&rfds))
{
//listensock happened read events
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listensock,(struct sockaddr*)&client,&len);
if(new_sock < 0)//accept failed
{
perror("accept");
continue;
}
else//accept success
{
printf("get a new client%s\n",inet_ntoa(client.sin_addr));
fflush(stdout);
int k = 1;
for(; k < array_size;++k)
{
if(array[k] < 0)
{
array[k] = new_sock;
if(new_sock > maxfd)
maxfd = new_sock;
break;
}
}
if(k == array_size)
{
close(new_sock);
}
}
}//j == 0
else if(j != 0 && FD_ISSET(array[j], &rfds))
{
//new_sock happend read events
char buf[1024];
ssize_t s = read(array[j],buf,sizeof(buf) - 1);
if(s > 0)//read success
{
buf[s] = 0;
printf("clientsay#%s\n",buf);
if(FD_ISSET(array[j],&wfds))
{
char *msg = "HTTP/1.0 200 OK <\r\n\r\n<html><h1>yingying beautiful</h1></html>\r\n";
write(array[j],msg,strlen(msg));
}
}
else if(0 == s)
{
printf("client quit!\n");
close(array[j]);
array[j] = -1;
}
else
{
perror("read");
close(array[j]);
array[j] = -1;
}
}//else j != 0
}
break;
}
}
}
return 0;
}
select的优缺点
优点:
(1)select的可移植性好,在某些unix下不支持poll.
(2)select对超时值提供了很好的精度,精确到微秒,而poll式毫秒。
缺点:
(1)单个进程可监视的fd数量被限制,默认是1024。
(2)需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
(3)对fd进行扫描时是线性扫描,fd剧增后,IO效率降低,每次调用都对fd进行线性扫描遍历,随着fd的增加会造成遍历速度慢的问题。
(4)select函数超时参数在返回时也是未定义的,考虑到可移植性,每次超时之后进入下一个select之前都要重新设置超时参数。
4.2 poll函数
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
fds
不同于select函数poll采用一个pollfd指针向内核传递需要关心的描述符及其相关事件。
struct pollfd {
int fd; /* file description */
short events; /* request events */
short revents; /* return events */
};
//合法事件如下
POLLIN 有数据可读。
POLLRDNORM 有普通数据可读。
POLLRDBAND 有优先数据可读。
POLLPRI 有紧迫数据可读。
POLLOUT 写数据不会导致阻塞。
POLLWRNORM 写普通数据不会导致阻塞。
POLLWRBAND 写优先数据不会导致阻塞。
POLLMSGSIGPOLL 消息可用。
//其中,revents还可使用如下事件
POLLER 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起事件。
POLLNVAL 指定的文件描述符非法。
nfds
标记数组中结构体元素的总个数。
timeout
超时时间 ,等于0表示非阻塞式等待,小于0表示阻塞式等待,大于0表示等待的时间。
返回值
成功时返回fds数组中事件就绪的文件描述符的个数
返回0表示超时时间到了。
返回-1表示调用失败,对应的错误码会被设置
poll函数实现原理如下:
(1)将需要关心的文件描述符放进fds数组中。
(2)调用poll函数。
(3)函数成功返回后根据返回值遍历fds数组,将关心的事件与结构体中的revents相与判断事件是否就绪。
(4)事件就绪执行相关操作。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<poll.h>
static void usage(const char *proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int start_up(const char*_ip,int _port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return 2;
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
return 3;
}
if(listen(sock,10) < 0)
{
perror("listen");
return 4;
}
return sock;
}
int main(int argc, char*argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int sock = start_up(argv[1],atoi(argv[2]));
struct pollfd peerfd[1024];
peerfd[0].fd = sock;
peerfd[0].events = POLLIN;
int nfds = 1;
int ret;
int maxsize = sizeof(peerfd)/sizeof(peerfd[0]);
int i = 1;
int timeout = -1;
for(; i < maxsize; ++i)
{
peerfd[i].fd = -1;
}
while(1)
{
switch(ret = poll(peerfd,nfds,timeout))
{
case 0:
printf("timeout...\n");
break;
case -1:
perror("poll");
break;
default:
{
if(peerfd[0].revents & POLLIN)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(sock,\
(struct sockaddr*)&client,&len);
printf("accept finish %d\n",new_sock);
if(new_sock < 0)
{
perror("accept");
continue;
}
printf("get a new client\n");
int j = 1;
for(; j < maxsize; ++j)
{
if(peerfd[j].fd < 0)
{
peerfd[j].fd = new_sock;
break;
}
}
if(j == maxsize)
{
printf("to many clients...\n");
close(new_sock);
}
peerfd[j].events = POLLIN;
if(j + 1 > nfds)
nfds = j + 1;
}
for(i = 1;i < nfds;++i)
{
if(peerfd[i].revents & POLLIN)
{
printf("read ready\n");
char buf[1024];
ssize_t s = read(peerfd[i].fd,buf, \
sizeof(buf) - 1);
if(s > 0)
{
buf[s] = 0;
printf("client say#%s",buf);
fflush(stdout);
peerfd[i].events = POLLOUT;
}
else if(s <= 0)
{
close(peerfd[i].fd);
peerfd[i].fd = -1;
}
else
{
}
}//i != 0
else if(peerfd[i].revents & POLLOUT)
{
char *msg = "HTTP/1.0 200 OK \
<\r\n\r\n<html><h1> \
yingying beautiful \
</h1></html>\r\n";
write(peerfd[i].fd,msg,strlen(msg));
close(peerfd[i].fd);
peerfd[i].fd = -1;
}
else
{
}
}//for
}//default
break;
}
}
return 0;
}
poll函数的优缺点
优点:
(1)不要求计算最大文件描述符+1的大小。
(2)应付大数量的文件描述符时比select要快。
(3)没有最大连接数的限制是基于链表存储的。
(4) 不需要每次都重新设置需要轮训的文件描述符列表。
缺点:
(1)大量的fd数组被整体复制于内核态和用户态之间,而不管这样的复制是不是有意义。
(2)同select相同的是调用结束后需要轮询来获取就绪描述符。
4.3 epoll函数
int epoll_create(int size);
生成一个epoll函数专用的文件描述符,其实是申请一个内核空间,用来存放你想关注的 socket fd 上是否发生以及发生了什么事件。 size 就是你在这个 Epoll fd 上能关注的最大 socket fd 数,大小自定,只要内存足够。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event );
控制文件描述符上的事件,包括注册,删除,修改等操作。
epfd : epoll的专用描述符。
op : 相关操作,通常用以下宏来表示。
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除⼀一个fd;
fd:op实施的对象。
event : 通知内核需要监听的事件。
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
events成员变量:
可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epfd : epoll特有的文件描述符。
events :从内核中的就绪队列中拷贝出就绪的文件描述符。不可以是空指针,内核只负责将数据拷贝到这里,不会为我们开辟空间。
maxevent : 告诉内核events有多大,一般不能超过epoll_create传递的size。
timeout : 函数超时时间,0表示非阻塞式等待,-1表示阻塞式等待,函数返回0表示已经超时。
epoll函数底层实现过程
首先epoll_create创建一个epoll文件描述符,底层同时创建一个红黑树,和一个就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl将会添加新的描述符,首先判断红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。
对于LT模式epoll_wait清空就绪链表之后会检查该文件描述符是哪一种模式,如果为LT模式,且必须该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表,epoll_wait马上返回。ET模式不会检查,只会调用一次
epoll函数是多路复用IO接口select和poll函数的增强版本。显著减少程序在大量并发连接中只有少量活跃的情况下CPU利用率,他不会复用文件描述符集合来传递结果,而迫使开发者每次等待事件之前都必须重新设置要等待的文件描述符集合,另外就是获取事件时无需遍历整个文件描述符集合,只需要遍历被内核异步唤醒加入ready队列的描述符集合就行了 。
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<sys/epoll.h>
static Usage(const char* proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int start_up(const char*_ip,int _port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
exit(3);
}
if(listen(sock,10)< 0)
{
perror("listen");
exit(4);
}
return sock;
}
int main(int argc, char*argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
int sock = start_up(argv[1],atoi(argv[2]));
int epollfd = epoll_create(256);
if(epollfd < 0)
{
perror("epoll_create");
return 5;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sock;
if(epoll_ctl(epollfd,EPOLL_CTL_ADD,sock,&ev) < 0)
{
perror("epoll_ctl");
return 6;
}
int evnums = 0;//epoll_wait return val
struct epoll_event evs[64];
int timeout = -1;
while(1)
{
switch(evnums = epoll_wait(epollfd,evs,64,timeout))
{
case 0:
printf("timeout...\n");
break;
case -1:
perror("epoll_wait");
break;
default:
{
int i = 0;
for(; i < evnums; ++i)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if(evs[i].data.fd == sock \
&& evs[i].events & EPOLLIN)
{
int new_sock = accept(sock, \
(struct sockaddr*)&client,&len);
if(new_sock < 0)
{
perror("accept");
continue;
}//if accept failed
else
{
printf("Get a new client[%s]\n", \
inet_ntoa(client.sin_addr));
ev.data.fd = new_sock;
ev.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,\
new_sock,&ev);
}//accept success
}//if fd == sock
else if(evs[i].data.fd != sock && \
evs[i].events & EPOLLIN)
{
char buf[1024];
ssize_t s = read(evs[i].data.fd,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s] = 0;
printf("client say#%s",buf);
ev.data.fd = evs[i].data.fd;
ev.events = EPOLLOUT;
epoll_ctl(epollfd,EPOLL_CTL_MOD, \
evs[i].data.fd,&ev);
}//s > 0
else
{
close(evs[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL, \
evs[i].data.fd,NULL);
}
}//fd != sock
else if(evs[i].data.fd != sock \
&& evs[i].events & EPOLLOUT)
{
char *msg = "HTTP/1.0 200 OK <\r\n\r\n<html><h1>yingying beautiful </h1></html>\r\n";
write(evs[i].data.fd,msg,strlen(msg));
close(evs[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL, \
evs[i].data.fd,NULL);
}//EPOLLOUT
else
{
}
}//for
}//default
break;
}//switch
}//while
return 0;
}
epoll函数的优缺点
优点:
epoll的优点:
(1)支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
(2)IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。
但是epoll不存在这个问题,它只会对”活跃”的socket进行 操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
(3)使用mmap加速内核与用户空间的消息传递。 这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。
(4)内核微调 这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。 比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小 — 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手 的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网 卡驱动架构。