Linux服务器编程
- OSI七层模型
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层 - TCP/IP模型
应用层
传输层
网络层
数据链路层
方法 - 多进程并发服务器
- 多线程并发服务器
- Select+多线程
笔者在工作中使用的时select+多线程的一种服务器设计模式。
select模式的优点就在与可以避免因为调用recv或者send时,因为套接字没有数据而导致的阻塞问题。select函数的参数如下:
extern int select (int __nfds, //0没有意义
fd_set *__restrict __readfds,//检查可读性
fd_set *__restrict __writefds,//检查可写性
fd_set *__restrict __exceptfds,//例外选项
struct timeval *__restrict __timeout);
fd_set 是socket的队列,可以通过如下的函数对其进行初始化的一些操作
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)//添加fd到队列fdsetp
#define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp)//从队列fdsetp中删除fd
#define FD_ISSET(fd, fdsetp)__FD_ISSET (fd, fdsetp)//检查句柄fd是否存在于队列fdsetp中
#define FD_ZERO(fdsetp)__FD_ZERO (fdsetp)//初始化fdsetp队列为空
使用实例:
for(; ; )
{
timeout.tv_sec=5;
timeout.tv_usec=0;
FD_ZERO(&fdR);
FD_SET(connfd,&fdR);
switch(select(connfd+1,&fdR,NULL,NULL,&timeout))
{
case -1:
DEV_PRINT(“no input\n”);
syslog(LOG_LOCAL0 | LOG_INFO,“TcpState: Tcp no input.”);
goto disconnect;
break;
case 0:
DEV_PRINT(“time is out\n”);
//syslog(LOG_LOCAL0 | LOG_INFO,“TcpState: Tcp connection time out.”);
//goto disconnect;
continue;
default:
if(FD_ISSET(connfd,&fdR)==0)
{
DEV_PRINT(“nothing to read\n”);
syslog(LOG_LOCAL0 | LOG_INFO,“TcpState: Tcp connection nothing to read.”);
goto disconnect;
}
//解析数据包
else
{
…
}
}
整个服务器的使用需要配合socket套接字的创建,绑定,监听,这部分是比较简单的,其次需要创建工作线程,来接收监听到的套接字,并存储于数组,配合select模型使用。
由于在连接数为0的情形下表明没有任何套接字,select函数会立刻返回,这将导致工作者线程成为一个毫无停顿的死循环,CPU的占用率马上达到100%,因此select函数的第一个参数默认会进行加1操作。select模型一个线程最多可以接受默认2048个连接,可以通过FD_SETSIZE宏来设置。
Select在工作一次时,需要查询俩次,至少遍历2次列表,这是它效率较低的原因之一。在大规模的网络连接方面,还是推荐使用IOCP或EPOLL模型.但是Select模型可以使用在诸如对战类游戏上,比如类似星际这种,因为它小巧易于实现,而且对战类游戏的网络连接量并不大。
select模型可以通过轮询机制支持无限多个连接。
注意:
1.那个最大的连接数是指每一个线程可以处理的连接数,当你有多个线程时,连接数是可以无限增长的,不过此时的效率就比较低。
2.关于发送操作writefds的问题,当套接字成功连接或者一个套接字刚刚成功接收信息时都会调用。
3.我们通常会创建一个套接字来进行监听,之后用accept返回的套接字进行通信。这里要注意一点,用于监听的套接字在没有新连接时也会进行writefds的操作。 - epoll方式
select模型最多支持的个数由FD_SETSIZE设置,FD_SETSIZE默认为2048,这对于大型的服务器来说是远远不够的,虽然通过修改该值可以满足大客户端连接请求,但是这样会导致网络效率的下降。
epoll的接口非常简单,一共就三个函数:
int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时(epoll使用epoll_wait监听)告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是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 */
};
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);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。