Linux服务器编程简介

Linux服务器编程的特点是异步高并发,代码不能阻塞、不能休眠,以提高服务器的并发效率。

给nginx写自定义的模块,就是典型的Linux服务器编程。

nginx-rtmp-module就是一个开源的nginx模块,它为nginx添加了rtmp协议的支持。

服务器的流程大概是这样:

worker进程->epoll循环->event事件回调->connection连接->http协议解析->

(CGI)->MVC后端框架->数据库。

CGI及之前的部分就是nginx这类服务器软件的功能范围,主要是实现从socket连接到http协议的处理。

MVC框架进一步处理http的正文内容,例如携带的html网页,并通过模版引擎生成应答的web页面,再逆着这个路径返回到用户,需要存储的信息则写入数据库。

master进程负责监控worker的状态,不处理具体的请求。

本文主要说的是CGI之前的部分,即从Linux API到nginx的http模块这个范围的编程特点。

Linux服务器端编程,与Linux内核的驱动模块编程有点类似,都属于异步高并发环境,都要注意保护共享数据和避免阻塞。

1,连接socket,

如果是客户端连接服务器,可以直接调用connect()。

但是,服务器连接更上游的服务器,则要把socket设置为非阻塞Non-block模式,这时的connect()会立即返回,然后要把socket文件描述符加入epoll并且监控写事件。

在epoll提示socket可写时,用getsocketopt()函数去获取连接的结果。

总之就是要把连接拆成发起和检测两步,中间等待的时间服务器可以去做别的,以最大化服务效率。

2,读数据,

用户的数据不一定是一次发完,可能需要几秒的时间,服务器也要多次去读,在这期间服务器也要同时去做别的,例如读取另一个用户的请求,不能阻塞等待。

也是把socket文件描述符加入epoll,并且准备一个读缓冲区,在epoll提示可读时把数据读到缓冲区,并且尝试使用http协议模块进行解析。

一般需要反复多次才可以读完一个http请求。

3,写数据,

写数据时要控制带宽,不能一下子把所有应答数据全写入socket,尤其是应答数据量很大的时候,例如一个高清晰度的视频文件。

写数据时socket可能返回EAGAIN错误码,提示等一会儿再写,这时要用epoll去监控socket是否可写。

控制写入速度的办法是使用定时器,不能像客户端程序一样使用sleep()。

例如每10毫秒写入100字节,客户端可以这么写:

int pos = 0;

while (pos < size) {

int n = size - pos >100?100:size -pos;

int ret = write(fd, buf + pos, n);

if(ret <= 0) {

//错误处理

break;

}

pos += ret;

usleep(10*1000);

}

服务器端因为还要同时为其他用户提供服务,所以不能休眠,而是设置一个定时器去写,每间隔10毫秒触发一次。

定时器的用户自定义数据部分,就是写缓冲区的参数和socket描述符。

struct data {

struct timer timer; //定时器,嵌在data结构体里,这样我们只需要申请一次内存就行

int fd; //socket描述符

uint8_t* buf; //缓冲区指针

int pos; //已写入的偏移量

int size; //数据总大小

};

还需要一个写数据的函数:

int write_data(struct data* d)

{

int n = d->size - d->pos;

n = n > 100? 100: n;

int ret = write(d->fd, d->buf + d->pos, n);

if (ret <= 0){

//错误处理

return ret;

}

d->pos += n;

//如果数据没写完,则把定时器设置到10毫秒之后,代码里的时间是按微秒写的

if (d->pos < d->size)

add_timer(&d->timer,10*1000);

else

data_free(d); //写完之后销毁

return 0;

}

struct timer的结构要包含红黑树节点,触发的时间,用户自定义数据的指针和自定义回调函数指针:

struct timer {

struct rbnode node;

int64_t time;

void*data;

int (handler)(voiddata);

};

在申请完struct data之后,假设指针为d则可以这么设置它的定时器:

d->timer.data =d;

d->timer.handler = write_data;

然后把它加入服务器的事件驱动框架里:

add_timer(&d->timer, 10*1000);

这样在工作进程worker的epoll循环中就可以检查定时器的时间,10毫秒之后开始写数据。

4,自旋锁spinlock,

在工作进程worker中,一定要使用trylock()去获取锁。例如获取监听用户接入的socket之前,要先获取锁。

如果trylock()成功则把该socket加入epoll监听,失败则继续做别的,说明有其他worker正在监听。

如果都监听则会epoll惊群,所以一个worker监听就可以了。

谁正在监听,谁持有锁。

其他worker尝试获取锁失败之后,不需要等待,一等待就阻塞了。

上一篇:epoll 原理


下一篇:sellect 、poll 、epoll