之前看过Socket,一直比较懒,没有总结一下,趁着有兴致赶紧写一下
Socket在Linux中被当作文件看待,对应的sock_fd也是一个文件符被操作,因为端口需要监听多个socket_fd,所以采用select机制来进行非阻塞监听。
直接上一段源码说明原理
//socket select example //源代码copied from http://blog.sina.com.cn/s/blog_5f84dc840100edej.html #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet.h> #include <arpa/inet.h> #define MYPORT 1234 //listening port #define BACKLOG 5 //max recieve client #define BUF_SIZE 200 int fd_A[BACKLOG]; //connected FD array int conn_amount; //connect number int main(int argc, char **argv) { int sock_fd, new_fd; //fd for listening, new connected fd struct sockaddr_in server_addr; //server addresss info struct sockaddr_in client_addr; //client address info socklen_t sin_size; int yes =1; char buf[BUF_SIZE]; int ret, i; //create a listing socket if((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("Create listening socket error "); exit(1); } //configure the listening socket //SO_REUSEADDR BOOL allow aport reuse if(setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) { perror("setsockopt error! "); exit(1); } server_addr.sin_family = AF_INET; server_addr.sin_port = htons(MYPORT); server_addr.sin_addr.s_addr = INADDR_ANY; memset(server_addr.sin_zero, '', sizeof(server_addr.sin_zero)); //bind sock_fd and server_addr if(bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("bind error! "); exit(1); } //begin listen if(listen(sock_fd, BACKLOG) == -1) { perror("listen error! "); exit(1); } //monitor fd set fd_set fdsr; //max file number int maxsock; //Select TimeOut time struct tim tv; conn_amount = 0; sin_size = sizeof(client_addr); maxsock = sock_fd; while(1) { FD_ZERO(&fdsr); FD_SET(sock_fd, &fdsr); tv.tv_sec = 30; tv.tv_usec = 0; //join the active socket handle into fdset for(i = 0; i < BACKLOG; i++) { if(fd_A[i] != 0) { FD_SET(fd_A[i], &fdsr); } } ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv); if(ret < 0) { perror("select error!"); break; } else if (ret == 0) { printf("timeoutn"); continue; } //monitor every sock_fd for(i = 0; i < conn_amount; i++) { if(FD_ISSET(fd_A[i], &fdsr) { ret = recv(fd_A[i], buf, sizeo(buf), 0); if(ret <= 0) { close(fd_A[i]); FD_CLR(fd_A[i], &fdsr); fd_A[i] = 0; } else { if(ret < BUF_SIZE) { memset(&buf[ret], '', 1); } } } } if(FD_ISSET(sock_fd, &fdsr)) { new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size); if(new_fd <= 0) { perror("accept socket error!"); continue; } if(conn_amount < BACKLOG) { fd_A[conn_amount++] = new_fd; if(new_fd > maxsock) { maxsock = new_fd; } } else { send(new_fd, "bye", 4, 0); close(new_fd); break; } } } for(i = 0; i < BACKLOG; i++) { if(fd_A[i] != 0) { close(fd_A[i]); } } return 0; }
上面的代码循环过程中不断地把用作listen 的sock_fd和用sock_fd监听到的new_fd加入到fdsr中,然后select去超时监听。如果返回值为正值证明监听到了可读信息,接着去检查对应的每一个fd是否在fdsr中,如果在就accept或读取信息。这种过程就可以进行非阻塞的监听,但是由于过程中select后需要将已经连接的fd循环一遍看是否有可读信息,在大规模的连接但数据偶发的情况下,这种循环检测会很慢且浪费资源。需要一种更好的解决方法——epoll
Epoll所以能够比select更加高效是因为
1.epoll不需要对每一个sock_fd做循环检测,避免了大量的连接时的循环延时.epoll将需要处理的文件符从内核态返回到用户态的一个链表中,当用户态需要处理时,只需要遍历链表中的事件即可。
2.epoll在监听之前就已经把句柄存储在内核中,每次添加新的监听句柄时才会向内核写入少量数据。而Select则是每次调用select函数都会将所有的监听的socket全部写入到内核态一遍,上万计的句柄写入到内核态,可能会有几十KB的大小,非常低效。
epoll有三个常用函数:
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, int maxevents, int timeout);
通过epoll_create创建一个epoll对象,size是最大的句柄数,系统允许的最大句柄数和机器内存有关,1G 大约时10亿的句柄
epoll_ctl函数则用于将某句柄加入或移出epoll对象
epoll_wait类似与select函数,用于监听超时控制
Linux的手册上对于epoll有这样的例子:
#define MAX_EVENTS 10 struct epoll_event ev, events[MAX_EVENTS]; int listen_sock, conn_sock, nfds, epollfd; /* Set up listening socket, 'listen_sock' (socket(), bind(), listen()) */ epollfd = epoll_create(10); if (epollfd == -1) { perror("epoll_create"); exit(EXIT_FAILURE); } ev.events = EPOLLIN; ev.data.fd = listen_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } for (;;) { nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_pwait"); exit(EXIT_FAILURE); } for (n = 0; n < nfds; ++n) { if (events[n].data.fd == listen_sock) { conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen); if (conn_sock == -1) { perror("accept"); exit(EXIT_FAILURE); } setnonblocking(conn_sock); ev.events = EPOLLIN | EPOLLET; ev.data.fd = conn_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) { perror("epoll_ctl: conn_sock"); exit(EXIT_FAILURE); } } else { do_use_fd(events[n].data.fd); } } }
结构非常简洁明了
epoll在内核初始化时,会开辟出epoll自己的内核高速cache区,这是一个slab层,申请了一块统一大小的内存区域,便于申请释放和赋值。
这些socket在cache中以红黑数的形式被管理保存,提升了查找插入和删除的速度。
Epoll有对应的两种模式, ET和LT。因为有事件发生时内核态会将事件句柄发送到用户态的list中,如果时ET 模式,用户态只会返回一次。而如果时LT模式,当用户态没有处理完该句柄时,下次epoll_wait后仍然会将该句柄提交到用户态。这个没有实践过,还不能体会这两种实际的应用场景区别。
socket的总结大致就是这么多。之后在实践中有新的学习和体会再补充。